Compare commits

..

3 Commits

Author SHA1 Message Date
Ludeeus
5e10d85f50 Console.Die 2021-02-16 17:14:19 +00:00
Ludeeus
d9283c17eb use startsWith 2021-02-16 17:11:09 +00:00
Ludeeus
dbc3bc81a7 Rewrite my links in markdown-element 2021-02-16 17:07:56 +00:00
512 changed files with 28157 additions and 48308 deletions

View File

@@ -84,8 +84,7 @@
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"],
"lit/attribute-value-entities": 0
"@typescript-eslint/no-shadow": ["error"]
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"

View File

@@ -1,8 +1,10 @@
name: Report a bug with the UI, Frontend or Lovelace
description: Report an issue related to the Home Assistant frontend.
about: Report an issue related to the Home Assistant frontend.
labels: bug
body:
- type: markdown
title: ""
issue_body: true
inputs:
- type: description
attributes:
value: |
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
@@ -17,34 +19,31 @@ body:
attributes:
label: Checklist
description: Please verify that you've followed these steps
options:
choices:
- label: I have updated to the latest available Home Assistant version.
required: true
- label: I have cleared the cache of my browser.
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
- type: markdown
- type: description
attributes:
value: |
## The problem
- type: textarea
validations:
required: true
attributes:
label: Describe the issue you are experiencing
required: true
description: Provide a clear and concise description of what the bug is.
- type: textarea
validations:
required: true
attributes:
label: Describe the behavior you expected
required: true
description: Describe what you expected to happen or it should look/behave.
- type: textarea
validations:
required: true
attributes:
label: Steps to reproduce the issue
required: true
description: |
Please tell us exactly how to reproduce your issue.
Provide clear and concise step by step instructions and add code snippets if needed.
@@ -53,37 +52,39 @@ body:
2.
3.
...
- type: markdown
- type: description
attributes:
value: |
## Environment
- type: input
validations:
required: true
attributes:
label: What version of Home Assistant Core has the issue?
required: true
placeholder: core-
description: >
Can be found in the Configuration panel -> Info.
- type: input
attributes:
label: What was the last working version of Home Assistant Core?
required: false
placeholder: core-
description: >
If known, otherwise leave blank.
- type: input
attributes:
label: In which browser are you experiencing the issue with?
required: false
placeholder: Google Chrome 88.0.4324.150
description: >
Provide the full name and don't forget to add the version!
- type: input
attributes:
label: Which operating system are you using to run this browser?
required: false
placeholder: macOS Big Sur (1.11)
description: >
Don't forget to add the version!
- type: markdown
- type: description
attributes:
value: |
# Details
@@ -91,31 +92,49 @@ body:
- type: textarea
attributes:
label: State of relevant entities
required: false
description: >
If your issue is about how an entity is shown in the UI, please add the
state and attributes for all situations. You can find this information
at Developer Tools -> States.
render: txt
value: |
```yaml
# Paste your state here.
```
- type: textarea
attributes:
label: Problem-relevant frontend configuration
required: false
description: >
An example configuration that caused the problem for you, e.g., the YAML
configuration of the used cards. Fill this out even if it seems
unimportant to you. Please be sure to remove personal information like
passwords, private URLs and other credentials.
render: yaml
value: |
```yaml
# Paste your YAML here.
```
- type: textarea
attributes:
label: Javascript errors shown in your browser console/inspector
description: >
If you come across any Javascript or other error logs, e.g., in your
browser console/inspector please provide them.
render: txt
- type: textarea
required: false
value: |
```txt
# Paste your logs here.
```
- type: description
attributes:
label: Additional information
description: >
value: |
## Additional information
- type: description
attributes:
value: |
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.
Please note, you can attach screenshots or screen recordings here,
by dragging and dropping files in the field below.

View File

@@ -7,7 +7,7 @@ on:
branches:
- dev
paths:
- src/translations/en.json
- translations/en.json
env:
NODE_VERSION: 12

View File

@@ -85,11 +85,6 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@@ -10,8 +10,6 @@ require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
gulp.task(
"develop-hassio",
@@ -22,8 +20,6 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@@ -36,8 +32,6 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
...// Don't compress running tests

View File

@@ -266,7 +266,6 @@ gulp.task(taskName, function () {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
delete data.supervisor;
return data;
})
)
@@ -343,62 +342,6 @@ gulp.task(
}
);
gulp.task("build-translation-fragment-supervisor", function () {
return gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(gulp.dest(workDir + "/supervisor"));
});
gulp.task("build-translation-flatten-supervisor", function () {
return gulp
.src(workDir + "/supervisor/*.json")
.pipe(
transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(gulp.dest(outDir));
});
gulp.task("build-translation-write-metadata", function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
"build-translations",
gulp.series(
@@ -410,20 +353,42 @@ gulp.task(
gulp.parallel(...splitTasks),
"build-flattened-translations",
"build-translation-fingerprints",
"build-translation-write-metadata"
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(
"clean-translations",
"ensure-translations-build-dir",
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",
"build-translation-flatten-supervisor",
"build-translation-fingerprints",
"build-translation-write-metadata"
function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
}
)
);

View File

@@ -137,12 +137,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false,
latestBuild: true,
})
).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
).watch({}, doneHandler());
});
gulp.task("webpack-prod-hassio", () =>

View File

@@ -34,7 +34,6 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"

View File

@@ -1,7 +1,7 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
const log = require("fancy-log");
@@ -68,7 +68,7 @@ const createWebpackConfig = ({
],
},
plugins: [
new WebpackManifestPlugin({
new ManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),

View File

@@ -48,7 +48,7 @@ class HcCast extends LitElement {
protected render(): TemplateResult {
if (this.lovelaceConfig === undefined) {
return html`<hass-loading-screen no-toolbar></hass-loading-screen>`;
return html` <hass-loading-screen no-toolbar></hass-loading-screen>> `;
}
const error =

View File

@@ -100,7 +100,7 @@ class HcLayout extends LitElement {
display: block;
margin: 0;
}
.hero {
border-radius: 4px 4px 0 0;
}

View File

@@ -35,12 +35,11 @@ class HcLovelace extends LitElement {
}
const lovelace: Lovelace = {
config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
locale: this.hass.locale,
language: "en",
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
@@ -95,7 +94,6 @@ class HcLovelace extends LitElement {
return css`
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;

View File

@@ -221,17 +221,11 @@ export class HcMain extends HassElement {
}
private async _generateLovelaceConfig() {
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
const { generateLovelaceConfigFromHass } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: false,
},
"original-states"
)
await generateLovelaceConfigFromHass(this.hass!)
);
}

View File

@@ -246,15 +246,11 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
"light.living_room_lights": {
entity_id: "light.living_room_lights",
state: "on",
state: "off",
attributes: {
min_mireds: 111,
max_mireds: 400,
brightness: 175,
color_temp: 300,
supported_color_modes: ["brightness", "color_temp"],
friendly_name: "Living Room Lights",
color_mode: "color_temp",
supported_features: 55,
},
},
@@ -267,27 +263,13 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
},
"light.kitchen_lights": {
entity_id: "light.kitchen_lights",
state: "on",
attributes: {
min_mireds: 111,
max_mireds: 400,
brightness: 200,
rgb_color: [255, 175, 96],
supported_color_modes: ["brightness", "color_temp", "rgb"],
color_mode: "rgb",
friendly_name: "Kitchen Lights",
supported_features: 55,
},
},
"light.lifx5": {
entity_id: "light.lifx5",
state: "off",
attributes: {
supported_color_modes: ["brightness"],
friendly_name: "Garage Lights",
friendly_name: "Kitchen Lights",
supported_features: 1,
},
},
"sensor.plexspy": {
entity_id: "sensor.plexspy",
state: "0",
@@ -500,6 +482,16 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
icon: "hademo:history",
},
},
"light.lifx5": {
entity_id: "light.lifx5",
state: "on",
attributes: {
min_mireds: 111,
max_mireds: 400,
friendly_name: "Garage Lights",
supported_features: 55,
},
},
"sensor.alok_to_home": {
entity_id: "sensor.alok_to_home",
state: "41",

View File

@@ -1114,9 +1114,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
min_mireds: 153,
max_mireds: 500,
brightness: 63,
color_temp: 200,
supported_color_modes: ["brightness", "color_temp", "rgb"],
color_mode: "color_temp",
friendly_name: "Upstairs lights",
supported_features: 63,
custom_ui_state_card: "state-card-custom-ui",
@@ -1128,7 +1125,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
friendly_name: "Walk in closet lights",
supported_features: 41,
supported_color_modes: ["brightness", "color_temp"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:wall-sconce",
},
@@ -1140,8 +1136,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
brightness: 254,
friendly_name: "Outdoor lights",
supported_features: 41,
supported_color_modes: ["brightness"],
color_mode: "brightness",
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:wall-sconce",
},
@@ -1154,8 +1148,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
max_mireds: 500,
brightness: 128,
color_temp: 366,
supported_color_modes: ["brightness", "color_temp", "rgb"],
color_mode: "color_temp",
effect_list: ["colorloop"],
friendly_name: "Downstairs lights",
supported_features: 63,
@@ -1315,7 +1307,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
min_mireds: 153,
max_mireds: 500,
supported_color_modes: ["brightness", "color_temp"],
is_deconz_group: false,
friendly_name: "Bedside Lamp",
supported_features: 63,
@@ -1329,7 +1320,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
min_mireds: 153,
max_mireds: 500,
supported_color_modes: ["brightness", "color_temp"],
is_deconz_group: false,
friendly_name: "Floorlamp Reading Light",
supported_features: 43,
@@ -1345,8 +1335,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
max_mireds: 500,
brightness: 128,
color_temp: 366,
supported_color_modes: ["brightness", "color_temp", "rgb"],
color_mode: "color_temp",
effect_list: ["colorloop"],
is_deconz_group: false,
friendly_name: "Hallway window light",
@@ -1361,7 +1349,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
brightness: 77,
is_deconz_group: false,
supported_color_modes: ["brightness"],
friendly_name: "Isa Ceiling Light",
supported_features: 41,
custom_ui_state_card: "state-card-custom-ui",
@@ -1376,8 +1363,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
max_mireds: 500,
brightness: 150,
color_temp: 366,
supported_color_modes: ["brightness", "color_temp"],
color_mode: "color_temp",
effect_list: ["colorloop"],
is_deconz_group: false,
friendly_name: "Floorlamp",
@@ -1392,7 +1377,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
friendly_name: "Bedroom Ceiling Light",
supported_features: 41,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:ceiling-light",
},
@@ -1403,7 +1387,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
attributes: {
friendly_name: "Nightlight",
supported_features: 17,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:lamp",
},
@@ -1770,7 +1753,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
power_consumption: 2.2,
friendly_name: "Upstairs Hallway Light",
supported_features: 33,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:ceiling-light",
},
@@ -1786,7 +1768,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
power_consumption: 0,
friendly_name: "Dining Room Light",
supported_features: 33,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:ceiling-light",
},
@@ -1802,7 +1783,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
power_consumption: 0,
friendly_name: "Living room Spotlights",
supported_features: 33,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:track-light",
},
@@ -1819,7 +1799,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
power_consumption: 2.5,
friendly_name: "Passage Lights",
supported_features: 33,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:track-light",
},
@@ -1864,7 +1843,6 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
power_consumption: 37.4,
friendly_name: "Kitchen Lights",
supported_features: 33,
supported_color_modes: ["brightness"],
custom_ui_state_card: "state-card-custom-ui",
icon: "mdi:track-light",
},

View File

@@ -15,10 +15,6 @@ class DemoCard extends PolymerElement {
margin: 0 0 20px;
color: var(--primary-color);
}
h2 small {
font-size: 0.5em;
color: var(--primary-text-color);
}
#card {
max-width: 400px;
width: 100vw;
@@ -38,12 +34,7 @@ class DemoCard extends PolymerElement {
}
}
</style>
<h2>
[[config.heading]]
<template is="dom-if" if="[[_size]]">
<small>(size [[_size]])</small>
</template>
</h2>
<h2>[[config.heading]]</h2>
<div class="root">
<div id="card"></div>
<template is="dom-if" if="[[showConfig]]">
@@ -64,9 +55,6 @@ class DemoCard extends PolymerElement {
observer: "_configChanged",
},
showConfig: Boolean,
_size: {
type: Number,
},
};
}
@@ -82,17 +70,6 @@ class DemoCard extends PolymerElement {
const el = this._createCardElement(safeLoad(config.config)[0]);
card.appendChild(el);
this._getSize(el);
}
async _getSize(el) {
await customElements.whenDefined(el.localName);
if (!("getCardSize" in el)) {
this._size = undefined;
return;
}
this._size = await el.getCardSize();
}
_createCardElement(cardConfig) {

View File

@@ -1,349 +0,0 @@
import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_step: "action/2",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-25T04:36:51.223693+00:00",
finish: "2021-03-25T04:36:51.266132+00:00",
},
trigger: "state of input_boolean.toggle_1",
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"condition/0": [
{
path: "condition/0",
timestamp: "2021-03-25T04:36:51.228243+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-24T19:03:59.141440+00:00",
last_updated: "2021-03-24T19:03:59.141440+00:00",
context: {
id: "5d0918eb379214d07554bdab6a08bcff",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-25T04:36:51.220696+00:00",
last_updated: "2021-03-25T04:36:51.220696+00:00",
context: {
id: "664d6d261450a9ecea6738e97269a149",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
},
result: {
result: true,
},
},
],
"action/0": [
{
path: "action/0",
timestamp: "2021-03-25T04:36:51.243018+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-24T19:03:59.141440+00:00",
last_updated: "2021-03-24T19:03:59.141440+00:00",
context: {
id: "5d0918eb379214d07554bdab6a08bcff",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-25T04:36:51.220696+00:00",
last_updated: "2021-03-25T04:36:51.220696+00:00",
context: {
id: "664d6d261450a9ecea6738e97269a149",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
context: {
id: "6cfcae368e7b3686fad6c59e83ae76c9",
parent_id: "664d6d261450a9ecea6738e97269a149",
user_id: null,
},
},
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_4"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/1": [
{
path: "action/1",
timestamp: "2021-03-25T04:36:51.252406+00:00",
result: {
choice: 0,
},
},
],
"action/1/choose/0": [
{
path: "action/1/choose/0",
timestamp: "2021-03-25T04:36:51.254569+00:00",
result: {
result: true,
},
},
],
"action/1/choose/0/conditions/0": [
{
path: "action/1/choose/0/conditions/0",
timestamp: "2021-03-25T04:36:51.254697+00:00",
result: {
result: true,
},
},
],
"action/1/choose/0/sequence/0": [
{
path: "action/1/choose/0/sequence/0",
timestamp: "2021-03-25T04:36:51.257360+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_2"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/1/choose/0/sequence/1": [
{
path: "action/1/choose/0/sequence/1",
timestamp: "2021-03-25T04:36:51.260658+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_3"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/2": [
{
path: "action/2",
timestamp: "2021-03-25T04:36:51.264159+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_4"],
},
},
running_script: false,
limit: 10,
},
},
],
},
config: {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
condition: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
action: [
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
{
choose: [
{
alias: "If toggle 3 is on",
conditions: [
{
condition: "template",
value_template:
"{{ is_state('input_boolean.toggle_3', 'on') }}",
},
],
sequence: [
{
service: "input_boolean.toggle",
alias: "Toggle 2 while 3 is on",
target: {
entity_id: "input_boolean.toggle_2",
},
},
{
service: "input_boolean.toggle",
alias: "Toggle 3",
target: {
entity_id: "input_boolean.toggle_3",
},
},
],
},
],
default: [
{
service: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
},
},
],
},
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
],
mode: "single",
},
context: {
id: "6cfcae368e7b3686fad6c59e83ae76c9",
parent_id: "664d6d261450a9ecea6738e97269a149",
user_id: null,
},
script_execution: "finished",
},
logbookEntries: [
{
name: "Ensure Party mode",
message: "has been triggered by state of input_boolean.toggle_1",
source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles",
context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
when: "2021-03-25T04:36:51.240832+00:00",
domain: "automation",
},
{
when: "2021-03-25T04:36:51.249828+00:00",
name: "Toggle 4",
state: "on",
entity_id: "input_boolean.toggle_4",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.258947+00:00",
name: "Toggle 2",
state: "on",
entity_id: "input_boolean.toggle_2",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.261806+00:00",
name: "Toggle 3",
state: "off",
entity_id: "input_boolean.toggle_3",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.265246+00:00",
name: "Toggle 4",
state: "off",
entity_id: "input_boolean.toggle_4",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
],
};

View File

@@ -1,44 +0,0 @@
import { LogbookEntry } from "../../../../src/data/logbook";
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { DemoTrace } from "./types";
export const mockDemoTrace = (
tracePartial: Partial<AutomationTraceExtended>,
logbookEntries?: LogbookEntry[]
): DemoTrace => ({
trace: {
last_step: "",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-25T04:36:51.223693+00:00",
finish: "2021-03-25T04:36:51.266132+00:00",
},
trigger: "mocked trigger",
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
changed_variables: {
trigger: {
description: "mocked trigger",
},
},
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
},
config: {
trigger: [],
action: [],
},
context: {
id: "abcd",
},
script_execution: "finished",
...tracePartial,
},
logbookEntries: logbookEntries || [],
});

View File

@@ -1,214 +0,0 @@
import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_step: "action/3",
run_id: "1",
state: "stopped",
timestamp: {
start: "2021-03-14T06:07:01.768006+00:00",
finish: "2021-03-14T06:07:53.287525+00:00",
},
trigger: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
domain: "automation",
item_id: "1614732497392",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"action/0": [
{
path: "action/0",
timestamp: "2021-03-14T06:07:01.771038+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
},
},
],
"action/1": [
{ path: "action/1", timestamp: "2021-03-14T06:07:01.875316+00:00" },
],
"action/2": [
{
path: "action/2",
timestamp: "2021-03-14T06:07:53.195013+00:00",
changed_variables: {
wait: {
remaining: null,
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:07:53.186755+00:00",
last_updated: "2021-03-14T06:07:53.186755+00:00",
context: {
id: "b2308cc91d509ea8e0c623331ab178d6",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
},
},
],
"action/3": [
{
path: "action/3",
timestamp: "2021-03-14T06:07:53.196014+00:00",
},
],
},
config: {
mode: "restart",
max_exceeded: "silent",
trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
action: [
{
service: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
},
{
wait_for_trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "on",
to: "off",
},
],
},
{
delay: 0,
},
{
service: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},
},
],
id: "1614732497392",
alias: "Auto Elgato",
description: "",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
script_execution: "finished",
},
logbookEntries: [
{
name: "Auto Elgato",
message:
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use",
source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato",
when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation",
},
{
when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air",
state: "on",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
{
when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air",
state: "off",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
],
};

View File

@@ -1,7 +0,0 @@
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { LogbookEntry } from "../../../../src/data/logbook";
export interface DemoTrace {
trace: AutomationTraceExtended;
logbookEntries: LogbookEntry[];
}

View File

@@ -1,102 +0,0 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeAction } from "../../../src/data/script_i18n";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
const actions = [
{ wait_template: "{{ true }}", alias: "Something with an alias" },
{ delay: "0:05" },
{ wait_template: "{{ true }}" },
{
condition: "template",
value_template: "{{ true }}",
},
{ event: "happy_event" },
{
device_id: "abcdefgh",
domain: "plex",
entity_id: "media_player.kitchen",
},
{ scene: "scene.kitchen_morning" },
{
wait_for_trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
},
{
variables: {
hello: "world",
},
},
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
];
@customElement("demo-automation-describe-action")
export class DemoAutomationDescribeAction extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Actions">
${actions.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.action {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-action": DemoAutomationDescribeAction;
}
}

View File

@@ -1,65 +0,0 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeCondition } from "../../../src/data/automation_i18n";
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
{ condition: "state" },
{ condition: "numeric_state" },
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise" },
{ condition: "zone" },
{ condition: "time" },
{ condition: "template" },
];
@customElement("demo-automation-describe-condition")
export class DemoAutomationDescribeCondition extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Conditions">
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.condition {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-condition": DemoAutomationDescribeCondition;
}
}

View File

@@ -1,68 +0,0 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeTrigger } from "../../../src/data/automation_i18n";
const triggers = [
{ platform: "state" },
{ platform: "mqtt" },
{ platform: "geo_location" },
{ platform: "homeassistant" },
{ platform: "numeric_state" },
{ platform: "sun" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{ platform: "zone" },
{ platform: "tag" },
{ platform: "time" },
{ platform: "template" },
{ platform: "event" },
];
@customElement("demo-automation-describe-trigger")
export class DemoAutomationDescribeTrigger extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Triggers">
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.trigger {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-trigger": DemoAutomationDescribeTrigger;
}
}

View File

@@ -1,87 +0,0 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { mockDemoTrace } from "../data/traces/mock-demo-trace";
import { DemoTrace } from "../data/traces/types";
const traces: DemoTrace[] = [
mockDemoTrace({ state: "running" }),
mockDemoTrace({ state: "debugged" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_conditions" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
mockDemoTrace({ state: "stopped", script_execution: "aborted" }),
mockDemoTrace({
state: "stopped",
script_execution: "error",
error: 'Variable "beer" cannot be None',
}),
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
];
@customElement("demo-automation-trace-timeline")
export class DemoAutomationTraceTimeline extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace-timeline
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace-timeline": DemoAutomationTraceTimeline;
}
}

View File

@@ -1,98 +0,0 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
internalProperty,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace";
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
@customElement("demo-automation-trace")
export class DemoAutomationTrace extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() private _selected = {};
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace, idx) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
></hat-script-graph>
<hat-trace-timeline
allowPick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
.card-content > * {
margin-right: 16px;
}
.card-content > *:last-child {
margin-right: 0;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace": DemoAutomationTrace;
}
}

View File

@@ -49,100 +49,6 @@ const ENTITIES = [
];
const CONFIGS = [
{
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
`,
},
{
heading: "Default Grid with title",
config: `
- type: grid
title: Kitchen
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: "Columns 4",
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
`,
},
{
heading: "Columns 2",
config: `
- type: grid
columns: 2
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
`,
},
{
heading: "Columns 1",
config: `
- type: grid
columns: 1
cards:
- type: entity
entity: light.kitchen_lights
`,
},
{
heading: "Vertical Stack",
config: `
@@ -193,9 +99,45 @@ 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
`,
},
];
@customElement("demo-hui-grid-and-stack-card")
@customElement("demo-hui-stack-card")
class DemoStack extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
@@ -213,8 +155,4 @@ class DemoStack extends LitElement {
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-hui-grid-and-stack-card": DemoStack;
}
}
customElements.define("demo-hui-stack-card", DemoStack);

View File

@@ -1,350 +0,0 @@
import {
customElement,
html,
css,
internalProperty,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import "../../../src/panels/config/integrations/ha-integration-card";
import "../../../src/panels/config/integrations/ha-ignored-config-entry-card";
import "../../../src/panels/config/integrations/ha-config-flow-card";
import type {
ConfigEntryExtended,
DataEntryFlowProgressExtended,
} from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit-html/directives/class-map";
const createConfigEntry = (
title: string,
override: Partial<ConfigEntryExtended> = {}
): ConfigEntryExtended => ({
entry_id: title,
domain: "esphome",
localized_domain_name: "ESPHome",
title,
source: "zeroconf",
state: "loaded",
connection_class: "local_push",
supports_options: false,
supports_unload: true,
disabled_by: null,
reason: null,
...override,
});
const createManifest = (
isCustom: boolean,
isCloud: boolean,
name = "ESPHome"
): IntegrationManifest => ({
name,
domain: "esphome",
is_built_in: !isCustom,
config_flow: false,
documentation: "https://www.home-assistant.io/integrations/esphome/",
iot_class: isCloud ? "cloud_polling" : "local_polling",
});
const loadedEntry = createConfigEntry("Loaded");
const nameAsDomainEntry = createConfigEntry("ESPHome");
const longNameEntry = createConfigEntry(
"Entry with a super long name that is going to the next line"
);
const configPanelEntry = createConfigEntry("Config Panel", {
domain: "mqtt",
localized_domain_name: "MQTT",
});
const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error",
});
const migrationErrorEntry = createConfigEntry("Migration Error", {
state: "migration_error",
});
const setupRetryEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
});
const setupRetryReasonEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
reason: "connection_error",
});
const setupRetryReasonMissingKeyEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
reason: "resolve_error",
});
const failedUnloadEntry = createConfigEntry("Failed Unload", {
state: "failed_unload",
});
const notLoadedEntry = createConfigEntry("Not Loaded", { state: "not_loaded" });
const disabledEntry = createConfigEntry("Disabled", {
state: "not_loaded",
disabled_by: "user",
});
const disabledFailedUnloadEntry = createConfigEntry(
"Disabled - Failed Unload",
{
state: "failed_unload",
disabled_by: "user",
}
);
const configFlows: DataEntryFlowProgressExtended[] = [
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "roku",
context: {
source: "ssdp",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Living room Roku",
},
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "hue",
context: {
source: "reauth",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Philips Hue",
},
];
const configEntries: Array<{
items: ConfigEntryExtended[];
is_custom?: boolean;
disabled?: boolean;
highlight?: string;
}> = [
{ items: [loadedEntry] },
{ items: [configPanelEntry] },
{ items: [optionsFlowEntry] },
{ items: [nameAsDomainEntry] },
{ items: [longNameEntry] },
{ items: [setupErrorEntry] },
{ items: [migrationErrorEntry] },
{ items: [setupRetryEntry] },
{ items: [setupRetryReasonEntry] },
{ items: [setupRetryReasonMissingKeyEntry] },
{ items: [failedUnloadEntry] },
{ items: [notLoadedEntry] },
{
items: [
loadedEntry,
setupErrorEntry,
migrationErrorEntry,
longNameEntry,
setupRetryEntry,
failedUnloadEntry,
notLoadedEntry,
disabledEntry,
nameAsDomainEntry,
configPanelEntry,
optionsFlowEntry,
],
},
{ disabled: true, items: [disabledEntry] },
{ disabled: true, items: [disabledFailedUnloadEntry] },
{
disabled: true,
items: [disabledEntry, disabledFailedUnloadEntry],
},
{
items: [loadedEntry, configPanelEntry],
highlight: "Loaded",
},
];
const createEntityRegistryEntries = (
item: ConfigEntryExtended
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
entity_id: "binary_sensor.updater",
name: null,
icon: null,
platform: "updater",
},
];
const createDeviceRegistryEntries = (
item: ConfigEntryExtended
): DeviceRegistryEntry[] => [
{
entry_type: null,
config_entries: [item.entry_id],
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
name: "Tag Reader",
sw_version: null,
id: "mock-device-id",
identifiers: [],
via_device_id: null,
area_id: null,
name_by_user: null,
disabled_by: null,
},
];
@customElement("demo-integration-card")
export class DemoIntegrationCard extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() isCustomIntegration = false;
@internalProperty() isCloud = false;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<div class="container">
<div class="filters">
<ha-formfield label="Custom Integration">
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
</ha-formfield>
<ha-formfield label="Relies on cloud">
<ha-switch @change=${this._toggleCloud}></ha-switch>
</ha-formfield>
</div>
<ha-ignored-config-entry-card
.hass=${this.hass}
.entry=${createConfigEntry("Ignored Entry")}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
></ha-ignored-config-entry-card>
${configFlows.map(
(flow) => html`
<ha-config-flow-card
.hass=${this.hass}
.flow=${flow}
.manifest=${createManifest(
this.isCustomIntegration,
this.isCloud,
flow.handler === "roku" ? "Roku" : "Philips Hue"
)}
></ha-config-flow-card>
`
)}
${configEntries.map(
(info) => html`
<ha-integration-card
class=${classMap({
highlight: info.highlight !== undefined,
})}
.hass=${this.hass}
domain="esphome"
.items=${info.items}
.manifest=${createManifest(
this.isCustomIntegration,
this.isCloud
)}
.entityRegistryEntries=${createEntityRegistryEntries(
info.items[0]
)}
.deviceRegistryEntries=${createDeviceRegistryEntries(
info.items[0]
)}
?disabled=${info.disabled}
.selectedConfigEntryId=${info.highlight}
></ha-integration-card>
`
)}
</div>
<div class="container">
<!-- One that is standalone to see how it increases height if height
not defined by other cards. -->
<ha-integration-card
.hass=${this.hass}
domain="esphome"
.items=${[
loadedEntry,
setupErrorEntry,
migrationErrorEntry,
setupRetryEntry,
failedUnloadEntry,
]}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
.entityRegistryEntries=${createEntityRegistryEntries(loadedEntry)}
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
></ha-integration-card>
</div>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
// Normally this string is loaded from backend
hass.addTranslations(
{
"component.esphome.config.error.connection_error":
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
},
"en"
);
}
private _toggleCustomIntegration() {
this.isCustomIntegration = !this.isCustomIntegration;
}
private _toggleCloud() {
this.isCloud = !this.isCloud;
}
static get styles() {
return css`
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin-bottom: 64px;
}
.container > * {
max-width: 500px;
}
ha-formfield {
margin: 8px 0;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-integration-card": DemoIntegrationCard;
}
}

View File

@@ -9,10 +9,13 @@ import {
} from "lit-element";
import "../../../src/components/ha-card";
import {
LightColorModes,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
SUPPORT_WHITE_VALUE,
} from "../../../src/data/light";
import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity";
@@ -29,8 +32,7 @@ const ENTITIES = [
getEntity("light", "kitchen_light", "on", {
friendly_name: "Brightness Light",
brightness: 200,
supported_color_modes: [LightColorModes.BRIGHTNESS],
color_mode: LightColorModes.BRIGHTNESS,
supported_features: SUPPORT_BRIGHTNESS,
}),
getEntity("light", "color_temperature_light", "on", {
friendly_name: "White Color Temperature Light",
@@ -38,96 +40,20 @@ const ENTITIES = [
color_temp: 75,
min_mireds: 30,
max_mireds: 150,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
],
color_mode: LightColorModes.COLOR_TEMP,
supported_features: SUPPORT_BRIGHTNESS + SUPPORT_COLOR_TEMP,
}),
getEntity("light", "color_hs_light", "on", {
friendly_name: "Color HS Light",
brightness: 255,
hs_color: [30, 100],
rgb_color: [30, 100, 255],
min_mireds: 30,
max_mireds: 150,
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
LightColorModes.HS,
],
color_mode: LightColorModes.HS,
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_rgb_ct_light", "on", {
friendly_name: "Color RGB + CT Light",
brightness: 255,
color_temp: 75,
min_mireds: 30,
max_mireds: 150,
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
LightColorModes.RGB,
],
color_mode: LightColorModes.COLOR_TEMP,
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_RGB_light", "on", {
getEntity("light", "color_effectslight", "on", {
friendly_name: "Color Effets Light",
brightness: 255,
rgb_color: [30, 100, 255],
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [LightColorModes.BRIGHTNESS, LightColorModes.RGB],
color_mode: LightColorModes.RGB,
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_rgbw_light", "on", {
friendly_name: "Color RGBW Light",
brightness: 255,
rgbw_color: [30, 100, 255, 125],
min_mireds: 30,
max_mireds: 150,
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
LightColorModes.RGBW,
],
color_mode: LightColorModes.RGBW,
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_rgbww_light", "on", {
friendly_name: "Color RGBWW Light",
brightness: 255,
rgbww_color: [30, 100, 255, 125, 10],
min_mireds: 30,
max_mireds: 150,
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
LightColorModes.RGBWW,
],
color_mode: LightColorModes.RGBWW,
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_xy_light", "on", {
friendly_name: "Color XY Light",
brightness: 255,
xy_color: [30, 100],
rgb_color: [30, 100, 255],
min_mireds: 30,
max_mireds: 150,
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
supported_color_modes: [
LightColorModes.BRIGHTNESS,
LightColorModes.COLOR_TEMP,
LightColorModes.XY,
],
color_mode: LightColorModes.XY,
hs_color: [30, 100],
white_value: 36,
supported_features:
SUPPORT_BRIGHTNESS +
SUPPORT_EFFECT +
SUPPORT_FLASH +
SUPPORT_COLOR +
SUPPORT_TRANSITION +
SUPPORT_WHITE_VALUE,
effect_list: ["random", "colorloop"],
}),
];
@@ -155,8 +81,4 @@ class DemoMoreInfoLight extends LitElement {
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-light": DemoMoreInfoLight;
}
}
customElements.define("demo-more-info-light", DemoMoreInfoLight);

View File

@@ -111,9 +111,29 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="Other Demos">
<div class="card-content intro"></div>
<template is="dom-repeat" items="[[_restDemos]]">
<ha-card header="More Info Demos">
<div class="card-content intro">
<p>
More info screens show up when an entity is clicked.
</p>
</div>
<template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
</ha-card>
<ha-card header="Util Demos">
<div class="card-content intro">
<p>
Test pages for our utility functions.
</p>
</div>
<template is="dom-repeat" items="[[_utilDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
@@ -158,9 +178,13 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeLovelace(_demos)",
},
_restDemos: {
_moreInfoDemos: {
type: Array,
computed: "_computeRest(_demos)",
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
},
};
}
@@ -213,8 +237,12 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui"));
}
_computeRest(demos) {
return demos.filter((demo) => !demo.includes("hui"));
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
}
}

View File

@@ -15,7 +15,6 @@ import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
@@ -24,8 +23,6 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: HassioAddonInfo[];
@@ -57,11 +54,7 @@ class HassioAddonRepositoryEl extends LitElement {
return html`
<div class="content">
<p class="description">
${this.supervisor.localize(
"store.no_results_found",
"repository",
repo.name
)}
No results found in "${repo.name}."
</p>
</div>
`;
@@ -90,13 +83,11 @@ class HassioAddonRepositoryEl extends LitElement {
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.installed")
? "New version available"
: "Add-on is installed"
: addon.available
? this.supervisor.localize("addon.not_installed")
: this.supervisor.localize("addon.not_available")}
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -11,20 +11,19 @@ import {
PropertyValues,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
HassioAddonInfo,
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -52,43 +51,58 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) private _addons?: HassioAddonInfo[];
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
@internalProperty() private _filter?: string;
public async refreshData() {
this._repos = undefined;
this._addons = undefined;
this._filter = undefined;
await reloadHassioAddons(this.hass);
await this._loadData();
}
protected render(): TemplateResult {
let repos: TemplateResult[] = [];
const repos: TemplateResult[] = [];
if (this.supervisor.addon.repositories) {
repos = this.addonRepositories(
this.supervisor.addon.repositories,
this.supervisor.addon.addons,
this._filter
);
if (this._repos) {
for (const repo of this._repos) {
const addons = this._addons!.filter(
(addon) => addon.repository === repo.slug
);
if (addons.length === 0) {
continue;
}
repos.push(html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
.filter=${this._filter!}
></hassio-addon-repository>
`);
}
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs}
hassio
main-page
supervisor
.tabs=${supervisorTabs}
>
<span slot="header">
${this.supervisor.localize("panel.store")}
</span>
<span slot="header">Add-on Store</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -98,15 +112,15 @@ class HassioAddonStore extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
${this.supervisor.localize("store.repositories")}
Repositories
</mwc-list-item>
<mwc-list-item>
${this.supervisor.localize("common.reload")}
Reload
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
${this.supervisor.localize("store.registries")}
Registries
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -127,9 +141,11 @@ class HassioAddonStore extends LitElement {
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
Missing add-ons? Enable advanced mode on
<a href="/profile" target="_top">
${this.supervisor.localize("store.missing_addons")}
your profile page
</a>
.
</div>
`
: ""}
@@ -139,46 +155,14 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate(this, "/hassio/store", true);
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
private addonRepositories = memoizeOne(
(
repositories: HassioAddonRepository[],
addons: HassioAddonInfo[],
filter?: string
) => {
return repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
);
return filteredAddons.length !== 0
? html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${filter!}
.supervisor=${this.supervisor}
></hassio-addon-repository>
`
: html``;
});
}
);
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositoriesClicked();
this._manageRepositories();
break;
case 1:
this.refreshData();
@@ -195,26 +179,30 @@ class HassioAddonStore extends LitElement {
}
}
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
private async _manageRepositories() {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
url,
repos: this._repos!,
loadData: () => this._loadData(),
});
}
private async _manageRegistries() {
showRegistriesDialog(this, { supervisor: this.supervisor });
showRegistriesDialog(this);
}
private async _loadData() {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
try {
const [addonsInfo, supervisor] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
]);
fireEvent(this, "supervisor-update", { supervisor });
this._repos = addonsInfo.repositories;
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
alert(extractApiErrorMessage(err));
}
}
private async _filterChanged(e) {

View File

@@ -25,7 +25,6 @@ import {
fetchHassioHardwareAudio,
HassioHardwareAudioDevice,
} from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -35,8 +34,6 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -51,16 +48,12 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card
.header=${this.supervisor.localize("addon.configuration.audio.header")}
>
<ha-card header="Audio">
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<paper-dropdown-menu
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
label="Input"
@iron-select=${this._setInputDevice}
>
<paper-listbox
@@ -71,17 +64,15 @@ class HassioAddonAudio extends LitElement {
${this._inputDevices &&
this._inputDevices.map((item) => {
return html`
<paper-item device=${item.device || ""}>
${item.name}
</paper-item>
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
label="Output"
@iron-select=${this._setOutputDevice}
>
<paper-listbox
@@ -102,7 +93,7 @@ class HassioAddonAudio extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
${this.supervisor.localize("common.save")}
Save
</ha-progress-button>
</div>
</ha-card>
@@ -161,7 +152,7 @@ class HassioAddonAudio extends LitElement {
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: this.supervisor.localize("addon.configuration.audio.default"),
name: "Default",
};
try {
@@ -198,7 +189,7 @@ class HassioAddonAudio extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";

View File

@@ -9,7 +9,6 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -21,28 +20,26 @@ import "./hassio-addon-network";
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||
(this.addon.schema && Object.keys(this.addon.schema).length);
const hasOptions =
this.addon.options && Object.keys(this.addon.options).length;
const hasSchema =
this.addon.schema && Object.keys(this.addon.schema).length;
return html`
<div class="content">
${hasConfiguration || this.addon.network || this.addon.audio
${(hasOptions && hasSchema) || this.addon.network || this.addon.audio
? html`
${hasConfiguration
${hasOptions && hasSchema
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-config>
`
: ""}
@@ -51,7 +48,6 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-network>
`
: ""}
@@ -60,12 +56,11 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-audio>
`
: ""}
`
: this.supervisor.localize("addon.configuration.no_configuration")}
: "This add-on does not expose configuration for you to mess with.... 👋"}
</div>
`;
}

View File

@@ -15,15 +15,11 @@ import {
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import {
@@ -32,7 +28,6 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -47,16 +42,12 @@ class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) private _configHasChanged = false;
@property({ type: Boolean }) private _valid = true;
@internalProperty() private _canShowSchema = false;
@internalProperty() private _showOptional = false;
@internalProperty() private _error?: string;
@internalProperty() private _options?: Record<string, unknown>;
@@ -65,70 +56,33 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
public computeLabel = (entry: HaFormSchema): string => {
return (
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name
);
};
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => {
return schema.filter((entry) => entry.name in options || entry.required);
}
);
protected render(): TemplateResult {
const showForm =
!this._yamlMode && this._canShowSchema && this.addon.schema;
const hasHiddenOptions =
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredShchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
<ha-card>
<div class="header">
<h2>
${this.supervisor.localize("addon.configuration.options.header")}
</h2>
<h2>Configuration</h2>
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode
? this.supervisor.localize(
"addon.configuration.options.edit_in_ui"
)
: this.supervisor.localize(
"addon.configuration.options.edit_in_yaml"
)}
${this._yamlMode ? "Edit in UI" : "Edit in YAML"}
</mwc-list-item>
<mwc-list-item class="warning">
${this.supervisor.localize("common.reset_defaults")}
Reset to defaults
</mwc-list-item>
</ha-button-menu>
</div>
</div>
<div class="card-content">
${showForm
${!this._yamlMode && this._canShowSchema && this.addon.schema
? html`<ha-form
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
.schema=${this.addon.schema}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
@@ -138,34 +92,14 @@ class HassioAddonConfig extends LitElement {
(this._canShowSchema && this.addon.schema) ||
this._valid
? ""
: html`
<div class="errors">
${this.supervisor.localize(
"addon.configuration.options.invalid_yaml"
)}
</div>
`}
: html` <div class="errors">Invalid YAML</div> `}
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
.label=${this.supervisor.localize(
"addon.configuration.options.show_unused_optional"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
>
${this.supervisor.localize("common.save")}
Save
</ha-progress-button>
</div>
</ha-card>
@@ -210,10 +144,6 @@ class HassioAddonConfig extends LitElement {
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private _configChanged(ev): void {
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
this._valid = true;
@@ -230,10 +160,10 @@ class HassioAddonConfig extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.reset_options.title"),
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
@@ -255,11 +185,9 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.common.update_available",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
@@ -283,14 +211,12 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = this.supervisor.localize(
"addon.configuration.options.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
@@ -347,10 +273,6 @@ class HassioAddonConfig extends LitElement {
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}

View File

@@ -19,7 +19,6 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -39,8 +38,6 @@ interface NetworkItemInput extends PaperInputElement {
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -58,30 +55,16 @@ class HassioAddonNetwork extends LitElement {
}
return html`
<ha-card
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
>
<ha-card header="Network">
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<table>
<tbody>
<tr>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>
${this.supervisor.localize("common.description")}
</th>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
${this._config!.map((item) => {
return html`
@@ -90,15 +73,13 @@ class HassioAddonNetwork extends LitElement {
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder="${this.supervisor.localize(
"addon.configuration.network.disabled"
)}"
placeholder="disabled"
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${this._computeDescription(item)}</td>
<td>${item.description}</td>
</tr>
`;
})}
@@ -107,10 +88,10 @@ class HassioAddonNetwork extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
${this.supervisor.localize("common.reset_defaults")}
Reset to defaults
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
${this.supervisor.localize("common.save")}
Save
</ha-progress-button>
</div>
</ha-card>
@@ -124,15 +105,6 @@ class HassioAddonNetwork extends LitElement {
}
}
private _computeDescription = (item: NetworkItem): string => {
return (
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description
);
};
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
@@ -175,14 +147,12 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_reset",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
@@ -211,14 +181,12 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}

View File

@@ -1,4 +1,3 @@
import "../../../../src/components/ha-card";
import {
css,
CSSResult,
@@ -20,14 +19,11 @@ import "../../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -85,11 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
this._error = this.supervisor.localize(
"addon.documentation.get_logs",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err
)}`;
}
}
}

View File

@@ -9,25 +9,17 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import {
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-error-screen";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
@@ -52,8 +44,6 @@ class HassioAddonDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@internalProperty() _error?: string;
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
@@ -68,19 +58,13 @@ class HassioAddonDashboard extends LitElement {
});
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
if (!this.addon) {
return html`<hass-loading-screen></hass-loading-screen>`;
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const addonTabs: PageNavigation[] = [
{
translationKey: "addon.panel.info",
name: "Info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
@@ -88,7 +72,7 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.documentation) {
addonTabs.push({
translationKey: "addon.panel.documentation",
name: "Documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
@@ -97,12 +81,12 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.version) {
addonTabs.push(
{
translationKey: "addon.panel.configuration",
name: "Configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
translationKey: "addon.panel.log",
name: "Log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
}
@@ -114,12 +98,11 @@ class HassioAddonDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route}
hassio
.tabs=${addonTabs}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
@@ -173,61 +156,30 @@ class HassioAddonDashboard extends LitElement {
}
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon");
if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
navigate(this, `/hassio/addon/${requestedAddon}`, true);
}
}
}
await this._routeDataChanged(this.route);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const pathSplit: string[] = ev.detail.path?.split("/");
const path: string = ev.detail.path;
if (!pathSplit || pathSplit.length === 0) {
if (!path) {
return;
}
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
if (path === "uninstall") {
window.history.back();
history.back();
} else {
await this._routeDataChanged();
await this._routeDataChanged(this.route);
}
}
protected updated(changedProperties) {
if (changedProperties.has("route") && !this.addon) {
this._routeDataChanged();
}
}
private async _routeDataChanged(): Promise<void> {
const addon = this.route.path.split("/")[1];
if (!addon) {
return;
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.split("/")[1];
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch (err) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
} catch {
this.addon = undefined;
}
}

View File

@@ -25,7 +25,6 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
@@ -44,13 +43,10 @@ import {
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
installHassioAddon,
restartHassioAddon,
setHassioAddonOption,
setHassioAddonSecurity,
startHassioAddon,
stopHassioAddon,
uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import {
@@ -58,7 +54,6 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../../src/data/hassio/common";
import { StoreAddon } from "../../../../src/data/supervisor/store";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -70,7 +65,6 @@ import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon";
@@ -80,6 +74,63 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick,
};
const PERMIS_DESC = {
stage: {
title: "Add-on Stage",
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
},
rating: {
title: "Add-on Security Rating",
description:
"Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Supervisor API Access",
description:
"The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@@ -94,23 +145,14 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
: undefined;
const metrics = [
{
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
description: "Add-on CPU Usage",
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("addon.dashboard.ram_usage"),
description: "Add-on RAM Usage",
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -120,64 +162,51 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
1
)}🎉"
>
<ha-card header="Update available! 🎉">
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title="${this.supervisor.localize(
"addon.dashboard.new_update_available",
"name",
this.addon.name,
"version",
this.addon.version_latest
)}"
.description="${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}"
.title="${this.addon.name} ${this.addon
.version_latest} is available"
.description="You are currently running version ${this.addon
.version}"
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available && addonStoreInfo
${!this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
<p>
This add-on is not compatible with the processor of
your device or the operating system you have installed
on your device.
</p>
`
: html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
<p>
You are running Home Assistant
${this.supervisor.core.version}, to update to this
version of the add-on you need at least version
${this.addon.homeassistant} of Home Assistant
</p>
`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button>
<ha-call-api-button
.hass=${this.hass}
.disabled=${!this.addon.available}
path="hassio/addons/${this.addon.slug}/update"
>
Update
</ha-call-api-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
Changelog
</mwc-button>
`
: ""}
@@ -188,19 +217,12 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected
? html`
<ha-card class="warning">
<h1 class="card-header">${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
</h1>
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
<div class="card-content">
${this.supervisor.localize("addon.dashboard.protection_mode.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>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
</mwc-button>
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
</div>
</div>
</ha-card>
@@ -217,18 +239,14 @@ class HassioAddonInfo extends LitElement {
${this._computeIsRunning
? html`
<ha-svg-icon
.title=${this.supervisor.localize(
"dashboard.addon_running"
)}
title="Add-on is running"
class="running"
.path=${mdiCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.title=${this.supervisor.localize(
"dashboard.addon_stopped"
)}
title="Add-on is stopped"
class="stopped"
.path=${mdiCircle}
></ha-svg-icon>
@@ -242,33 +260,21 @@ class HassioAddonInfo extends LitElement {
? html`
Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link"
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>)
(<span class="changelog-link">changelog</span>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>Changelog</span
>`}
</div>
<div class="description light-color">
${this.addon.description}.<br />
${this.supervisor.localize(
"addon.dashboard.visit_addon_page",
"name",
html`<a
href="${this.addon.url!}"
target="_blank"
rel="noreferrer"
>
${this.addon.name}
</a>`
)}
Visit
<a href="${this.addon.url!}" target="_blank" rel="noreferrer">
${this.addon.name} page</a
>
for details.
</div>
<div class="addon-container">
<div>
@@ -289,9 +295,7 @@ class HassioAddonInfo extends LitElement {
})}
@click=${this._showMoreInfo}
id="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
label="stage"
description=""
>
<ha-svg-icon
@@ -317,9 +321,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
label="host"
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
@@ -331,9 +333,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
label="hardware"
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
@@ -345,9 +345,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
label="hass"
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
@@ -359,12 +357,8 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
label="hassio"
.description=${this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
@@ -375,9 +369,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}"
label="docker"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
@@ -389,9 +381,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
label="host pid"
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
@@ -404,9 +394,7 @@ class HassioAddonInfo extends LitElement {
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
label="apparmor"
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
@@ -418,9 +406,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
label="auth"
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
@@ -432,9 +418,7 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
label="ingress"
description=""
>
<ha-svg-icon
@@ -455,14 +439,10 @@ class HassioAddonInfo extends LitElement {
>
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.boot.title"
)}
Start on boot
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
Make the add-on start during a system boot
</span>
<ha-switch
@change=${this._startOnBootToggled}
@@ -475,14 +455,10 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.watchdog.title"
)}
Watchdog
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.watchdog.description"
)}
This will start the add-on if it crashes
</span>
<ha-switch
@change=${this._watchdogToggled}
@@ -497,14 +473,11 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.auto_update.title"
)}
Auto update
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.auto_update.description"
)}
Auto update the add-on when there is a new
version available
</span>
<ha-switch
@change=${this._autoUpdateToggled}
@@ -514,22 +487,21 @@ class HassioAddonInfo extends LitElement {
</ha-settings-row>
`
: ""}
${!this._computeCannotIngressSidebar && this.addon.ingress
${this.addon.ingress
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.title"
)}
Show in sidebar
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.description"
)}
${this._computeCannotIngressSidebar
? "This option requires Home Assistant 0.92 or later."
: "Add this add-on to your sidebar"}
</span>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
</ha-settings-row>
@@ -539,14 +511,10 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.protected.title"
)}
Protection mode
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.protected.description"
)}
Blocks elevated system access from the add-on
</span>
<ha-switch
@change=${this._protectionToggled}
@@ -564,7 +532,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize("addon.dashboard.hostname")}
Hostname
</span>
<code slot="description">
${this.addon.hostname}
@@ -584,27 +552,24 @@ class HassioAddonInfo extends LitElement {
</div>
</div>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this.addon.version && addonStoreInfo && !this.addon.available
${!this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
This add-on is not compatible with the processor of your
device or the operating system you have installed on your
device.
</p>
`
: html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_version",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo!.homeassistant
)}
You are running Home Assistant
${this.supervisor.core.version}, to install this add-on you
need at least version ${this.addon.homeassistant} of Home
Assistant
</p>
`
: ""}
@@ -614,22 +579,24 @@ class HassioAddonInfo extends LitElement {
${this.addon.version
? this._computeIsRunning
? html`
<ha-progress-button
<ha-call-api-button
class="warning"
@click=${this._stopClicked}
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/stop"
>
${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
Stop
</ha-call-api-button>
<ha-call-api-button
class="warning"
@click=${this._restartClicked}
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
${this.supervisor.localize("addon.dashboard.restart")}
</ha-progress-button>
Restart
</ha-call-api-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
${this.supervisor.localize("addon.dashboard.start")}
Start
</ha-progress-button>
`
: html`
@@ -637,7 +604,7 @@ class HassioAddonInfo extends LitElement {
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
${this.supervisor.localize("addon.dashboard.install")}
Install
</ha-progress-button>
`}
</div>
@@ -652,9 +619,7 @@ class HassioAddonInfo extends LitElement {
rel="noopener"
>
<mwc-button>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
Open web UI
</mwc-button>
</a>
`
@@ -662,9 +627,7 @@ class HassioAddonInfo extends LitElement {
${this._computeShowIngressUI
? html`
<mwc-button @click=${this._openIngress}>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
Open web UI
</mwc-button>
`
: ""}
@@ -672,7 +635,7 @@ class HassioAddonInfo extends LitElement {
class="warning"
@click=${this._uninstallClicked}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
Uninstall
</ha-progress-button>
${this.addon.build
? html`
@@ -681,7 +644,7 @@ class HassioAddonInfo extends LitElement {
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
${this.supervisor.localize("addon.dashboard.rebuild")}
Rebuild
</ha-call-api-button>
`
: ""}`
@@ -741,21 +704,8 @@ class HassioAddonInfo extends LitElement {
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id;
showHassioMarkdownDialog(this, {
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
content:
id === "stage"
? this.supervisor.localize(
`addon.dashboard.capability.${id}.description`,
"icon_stable",
`<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
"icon_experimental",
`<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
"icon_deprecated",
`<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`
)
: this.supervisor.localize(
`addon.dashboard.capability.${id}.description`
),
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
@@ -808,11 +758,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
}
}
@@ -830,11 +778,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
}
}
@@ -852,11 +798,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
}
}
@@ -874,11 +818,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err
)}`;
}
}
@@ -896,11 +838,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
}
}
@@ -911,14 +851,12 @@ class HassioAddonInfo extends LitElement {
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"),
title: "Changelog",
content,
});
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.get_changelog"
),
title: "Failed to get addon changelog",
text: extractApiErrorMessage(err),
});
}
@@ -938,82 +876,13 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.install"),
title: "Failed to install addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _stopClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await stopHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "stop",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _restartClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await restartHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "stop",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _updateClicked(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: this.addon.name,
version: this.addon.version_latest,
snapshotParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
},
updateHandler: async () => await this._updateAddon(),
});
}
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
@@ -1022,17 +891,13 @@ class HassioAddonInfo extends LitElement {
this.hass,
this.addon.slug
);
if (!validate.valid) {
if (!validate.data.valid) {
await showConfirmationDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.start_invalid_config"
),
text: validate.message.split(" Got ")[0],
title: "Failed to start addon - configuration validation failed!",
text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: this.supervisor.localize(
"addon.dashboard.action_error.go_to_config"
),
dismissText: this.supervisor.localize("common.cancel"),
confirmText: "Go to configuration",
dismissText: "Cancel",
});
button.progress = false;
return;
@@ -1049,15 +914,9 @@ class HassioAddonInfo extends LitElement {
try {
await startHassioAddon(this.hass, this.addon.slug);
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "start",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.start"),
title: "Failed to start addon",
text: extractApiErrorMessage(err),
});
}
@@ -1095,9 +954,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.uninstall"
),
title: "Failed to uninstall addon",
text: extractApiErrorMessage(err),
});
}

View File

@@ -9,7 +9,6 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -19,8 +18,6 @@ import "./hassio-addon-logs";
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
@@ -31,7 +28,6 @@ class HassioAddonLogDashboard extends LitElement {
<div class="content">
<hassio-addon-logs
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
</div>

View File

@@ -15,7 +15,6 @@ import {
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
@@ -25,8 +24,6 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -51,9 +48,7 @@ class HassioAddonLogs extends LitElement {
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</ha-card>
`;
@@ -81,11 +76,7 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = this.supervisor.localize(
"addon.logs.get_logs",
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
}
}

View File

@@ -44,7 +44,7 @@ class HassioCardContent extends LitElement {
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img src="${this.iconImage}" .title=${this.iconTitle} />
<img src="${this.iconImage}" title="${this.iconTitle}" />
<div></div>
</div>
`

View File

@@ -26,9 +26,9 @@ class SupervisorMetric extends LitElement {
<span slot="heading">
${this.description}
</span>
<div slot="description" .title=${this.tooltip ?? ""}>
<div slot="description" title="${this.tooltip ?? ""}">
<span class="value">
${roundedValue} %
${roundedValue}%
</span>
<ha-bar
class="${classMap({
@@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
);
}
.value {
width: 48px;
width: 42px;
padding-right: 4px;
}
`;

View File

@@ -27,15 +27,17 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
<h1>Add-ons</h1>
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
? html`
<ha-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")}
the add-on store
</button>
to get started!
</div>
</ha-card>
`
@@ -56,16 +58,10 @@ class HassioAddons extends LitElement {
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize(
"dashboard.addon_stopped"
)
? "Add-on is stopped"
: addon.update_available!
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
? "New version available"
: "Add-on is running"}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"

View File

@@ -29,16 +29,13 @@ class HassioDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<span slot="header">Dashboard</span>
<div class="content">
<hassio-update
.hass=${this.hass}

View File

@@ -10,40 +10,28 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
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-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoreSupervisorError,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => {
return key === "os" ? version : `${key}-${version}`;
};
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -69,17 +57,13 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
<h1>
${this.supervisor.localize(
"common.update_available",
"count",
updatesAvailable
)}
🎉
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</h1>
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core,
"hassio/homeassistant/update",
`https://${
@@ -88,7 +72,6 @@ export class HassioUpdate extends LitElement {
)}
${this._renderUpdateCard(
"Supervisor",
"supervisor",
this.supervisor.supervisor,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
@@ -96,7 +79,6 @@ export class HassioUpdate extends LitElement {
${this.supervisor.host.features.includes("hassos")
? this._renderUpdateCard(
"Operating System",
"os",
this.supervisor.os,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
@@ -109,7 +91,6 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string
@@ -123,39 +104,22 @@ export class HassioUpdate extends LitElement {
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
<div class="update-heading">${name} ${object.version_latest}</div>
<div class="warning">
You are currently running version ${object.version}
</div>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>
${this.supervisor.localize("common.release_notes")}
</mwc-button>
<mwc-button>Release notes</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
${this.supervisor.localize("common.update")}
Update
</ha-progress-button>
</div>
</ha-card>
@@ -164,36 +128,12 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
title: `Update ${item.name}`,
text: `Are you sure you want to update ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
@@ -201,24 +141,13 @@ export class HassioUpdate extends LitElement {
return;
}
try {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
title: "Update failed",
text: extractApiErrorMessage(err),
});
}
@@ -226,13 +155,6 @@ export class HassioUpdate extends LitElement {
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -250,6 +172,9 @@ export class HassioUpdate extends LitElement {
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.warning {
color: var(--secondary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
@@ -257,13 +182,13 @@ export class HassioUpdate extends LitElement {
.card-actions {
text-align: right;
}
.errors {
color: var(--error-color);
padding: 16px;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];
}

View File

@@ -18,6 +18,7 @@ import {
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -34,7 +35,6 @@ import {
updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -51,8 +51,6 @@ export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _curTabIndex = 0;
@@ -75,8 +73,7 @@ export class DialogHassioNetwork extends LitElement
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this.supervisor = params.supervisor;
this._interfaces = params.supervisor.network.interfaces.sort((a, b) => {
this._interfaces = params.network.interfaces.sort((a, b) => {
return a.primary > b.primary ? -1 : 1;
});
this._interface = { ...this._interfaces[this._curTabIndex] };
@@ -107,7 +104,7 @@ export class DialogHassioNetwork extends LitElement
<div slot="heading">
<ha-header-bar>
<span slot="title">
${this.supervisor.localize("dialog.network.title")}
Network settings
</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
@@ -142,13 +139,7 @@ export class DialogHassioNetwork extends LitElement
? html`
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>
${this.supervisor.localize(
"dialog.network.connected_to",
"ssid",
this._interface?.wifi?.ssid
)}
</p>`
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
: ""}
<mwc-button
class="scan"
@@ -158,7 +149,7 @@ export class DialogHassioNetwork extends LitElement
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: this.supervisor.localize("dialog.network.scan_ap")}
: "Scan for accesspoints"}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
@@ -190,11 +181,7 @@ export class DialogHassioNetwork extends LitElement
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.open"
)}
>
<ha-formfield label="open">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -206,11 +193,7 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wep"
)}
>
<ha-formfield label="wep">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -220,11 +203,7 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wpa"
)}
>
<ha-formfield label="wpa-psk">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -258,21 +237,18 @@ export class DialogHassioNetwork extends LitElement
: ""}
${this._dirty
? html`<div class="warning">
${this.supervisor.localize("dialog.network.warning")}
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
</div>`
: ""}
</div>
<div class="buttons">
<mwc-button
.label=${this.supervisor.localize("common.cancel")}
@click=${this.closeDialog}
>
</mwc-button>
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: this.supervisor.localize("common.save")}
: "Save"}
</mwc-button>
</div>`;
}
@@ -309,9 +285,7 @@ export class DialogHassioNetwork extends LitElement
outlined
>
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.dhcp")}
>
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -321,9 +295,7 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.static")}
>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -333,10 +305,7 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.disabled")}
class="warning"
>
<ha-formfield label="Disabled" class="warning">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -352,7 +321,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="address"
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
label="IP address/Netmask"
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
@@ -361,7 +330,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="gateway"
.label=${this.supervisor.localize("dialog.network.gateway")}
label="Gateway address"
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
@@ -370,7 +339,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="nameservers"
.label=${this.supervisor.localize("dialog.network.dns_servers")}
label="DNS servers"
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
@@ -455,7 +424,7 @@ export class DialogHassioNetwork extends LitElement
);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.network.failed_to_change"),
title: "Failed to change network settings",
text: extractApiErrorMessage(err),
});
this._processing = false;
@@ -468,9 +437,10 @@ export class DialogHassioNetwork extends LitElement
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text: this.supervisor.localize("dialog.network.unsaved"),
confirmText: this.supervisor.localize("common.yes"),
dismissText: this.supervisor.localize("common.no"),
text:
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
confirmText: "yes",
dismissText: "no",
});
if (!confirm) {
this.requestUpdate("_interface");

View File

@@ -1,9 +1,9 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { NetworkInfo } from "../../../../src/data/hassio/network";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
supervisor: Supervisor;
network: NetworkInfo;
loadData: () => Promise<void>;
}

View File

@@ -22,18 +22,14 @@ import {
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
@@ -59,8 +55,8 @@ class HassioRegistriesDialog extends LitElement {
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? this.supervisor.localize("dialog.registries.title_add")
: this.supervisor.localize("dialog.registries.title_manage")
? "Add New Docker Registry"
: "Manage Docker Registries"
)}
>
<div class="form">
@@ -70,9 +66,7 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
.label=${this.supervisor.localize(
"dialog.registries.registry"
)}
label="Registry"
required
auto-validate
></paper-input>
@@ -80,9 +74,7 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
label="Username"
required
auto-validate
></paper-input>
@@ -90,9 +82,7 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
label="Password"
type="password"
required
auto-validate
@@ -104,7 +94,7 @@ class HassioRegistriesDialog extends LitElement {
)}
@click=${this._addNewRegistry}
>
${this.supervisor.localize("dialog.registries.add_registry")}
Add registry
</mwc-button>
`
: html`${this._registries?.length
@@ -113,16 +103,11 @@ class HassioRegistriesDialog extends LitElement {
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}</span
>Username: ${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
.title=${this.supervisor.localize(
"dialog.registries.remove"
)}
title="Remove"
slot="meta"
@click=${this._removeRegistry}
>
@@ -133,17 +118,11 @@ class HassioRegistriesDialog extends LitElement {
})
: html`
<mwc-list-item>
<span
>${this.supervisor.localize(
"dialog.registries.no_registries"
)}</span
>
<span>No registries configured</span>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
Add new registry
</mwc-button> `}
</div>
</ha-dialog>
@@ -155,9 +134,8 @@ class HassioRegistriesDialog extends LitElement {
this[`_${target.name}`] = target.value;
}
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
public async showDialog(_dialogParams: any): Promise<void> {
this._opened = true;
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
}
@@ -200,7 +178,7 @@ class HassioRegistriesDialog extends LitElement {
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_add"),
title: "Failed to add registry",
text: extractApiErrorMessage(err),
});
}
@@ -214,7 +192,7 @@ class HassioRegistriesDialog extends LitElement {
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
title: "Failed to remove registry",
text: extractApiErrorMessage(err),
});
}

View File

@@ -1,18 +1,10 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-registries";
export interface RegistriesDialogParams {
supervisor: Supervisor;
}
export const showRegistriesDialog = (
element: HTMLElement,
dialogParams: RegistriesDialogParams
): void => {
export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogParams,
dialogParams: {},
});
};

View File

@@ -17,9 +17,8 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
@@ -35,29 +34,27 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _repositories?: HassioAddonRepository[];
@internalProperty() private _dialogParams?: HassioRepositoryDialogParams;
@internalProperty() private _opened = false;
@internalProperty() private _prosessing = false;
@internalProperty() private _error?: string;
public async showDialog(
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
public async showDialog(_dialogParams: any): Promise<void> {
this._dialogParams = _dialogParams;
this._repos = _dialogParams.repos;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false;
this._error = "";
}
@@ -69,20 +66,14 @@ class HassioRepositoriesDialog extends LitElement {
);
protected render(): TemplateResult {
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return html``;
}
const repositories = this._filteredRepositories(this._repositories);
const repositories = this._filteredRepositories(this._repos);
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
heading="Manage add-on repositories"
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="form">
@@ -97,9 +88,7 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<mwc-icon-button
.slug=${repo.slug}
.title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
title="Remove"
@click=${this._removeRepository}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
@@ -116,23 +105,18 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input
class="flex-auto"
id="repository_input"
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
label="Add repository"
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
: "Add"}
</mwc-button>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this._dialogParams?.supervisor.localize("common.close")}
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
Close
</mwc-button>
</ha-dialog>
`;
@@ -163,11 +147,6 @@ class HassioRepositoriesDialog extends LitElement {
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
`,
];
}
@@ -188,25 +167,13 @@ class HassioRepositoriesDialog extends LitElement {
this._addRepository();
}
private async _loadData(): Promise<void> {
try {
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
this._repositories = addonsinfo.repositories;
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._prosessing = true;
const repositories = this._filteredRepositories(this._repositories!);
const repositories = this._filteredRepositories(this._repos);
const newRepositories = repositories.map((repo) => {
return repo.source;
});
@@ -216,7 +183,11 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
await this._loadData();
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
input.value = "";
} catch (err) {
@@ -227,7 +198,7 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug;
const repositories = this._filteredRepositories(this._repositories!);
const repositories = this._filteredRepositories(this._repos);
const repository = repositories.find((repo) => {
return repo.slug === slug;
});
@@ -246,7 +217,11 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
await this._loadData();
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
}

View File

@@ -1,10 +1,10 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { HassioAddonRepository } from "../../../../src/data/hassio/addon";
import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
supervisor: Supervisor;
url?: string;
repos: HassioAddonRepository[];
loadData: () => Promise<void>;
}
export const showRepositoriesDialog = (

View File

@@ -95,7 +95,7 @@ class HassioSnapshotDialog extends LitElement {
@internalProperty() private _snapshotPassword!: string;
@internalProperty() private _restoreHass = true;
@internalProperty() private _restoreHass: boolean | null | undefined = true;
public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
@@ -109,9 +109,6 @@ class HassioSnapshotDialog extends LitElement {
this._dialogParams = params;
this._onboarding = params.onboarding ?? false;
this.supervisor = params.supervisor;
if (!this._snapshot.homeassistant) {
this._restoreHass = false;
}
}
protected render(): TemplateResult {
@@ -137,17 +134,15 @@ class HassioSnapshotDialog extends LitElement {
(${this._computeSize})<br />
${this._formatDatetime(this._snapshot.date)}
</div>
${this._snapshot.homeassistant
? html`<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>`
: ""}
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>
${this._folders.length
? html`
<div>Folders:</div>
@@ -339,7 +334,7 @@ class HassioSnapshotDialog extends LitElement {
.map((folder) => folder.slug);
const data: {
homeassistant: boolean;
homeassistant: boolean | null | undefined;
addons: any;
folders: any;
password?: string;

View File

@@ -4,7 +4,6 @@ import {
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -14,25 +13,20 @@ import { HomeAssistant } from "../../../src/types";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
supervisor: Supervisor,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: supervisor.localize("common.restart_name", "name", addon.name),
text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.confirm_text"),
dismissText: supervisor.localize("common.cancel"),
title: addon.name,
text: "Do you want to restart the add-on with your changes?",
confirmText: "restart add-on",
dismissText: "no",
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err) {
showAlertDialog(element, {
title: supervisor.localize(
"common.failed_to_restart_name",
"name",
addon.name
),
title: "Failed to restart",
text: extractApiErrorMessage(err),
});
}

View File

@@ -1,208 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
@internalProperty()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
${this._dialogParams.supervisor.localize(
"dialog.update.snapshot"
)}
</span>
<span slot="description">
${this._dialogParams.supervisor.localize(
"dialog.update.create_snapshot",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.snapshotting",
"name",
this._dialogParams.name
)}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(
this.hass,
this._dialogParams!.snapshotParams
);
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null;
return;
}
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-update": DialogSupervisorUpdate;
}
}

View File

@@ -1,21 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
snapshotParams: any;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
dialogImport: () => import("./dialog-supervisor-update"),
dialogParams,
});
};

View File

@@ -3,9 +3,7 @@ import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element";
@@ -14,8 +12,6 @@ import { SupervisorBaseElement } from "./supervisor-base-element";
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@@ -44,10 +40,7 @@ export class HassioMain extends SupervisorBaseElement {
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
// Joakim - April 26, 2021
// Due to changes in behavior in Google Chrome, we changed navigate to fire on the top element
top.addEventListener("location-changed", (ev) =>
window.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
@@ -77,6 +70,9 @@ export class HassioMain extends SupervisorBaseElement {
}
protected render() {
if (!this.supervisor || !this.hass) {
return html``;
}
return html`
<hassio-router
.hass=${this.hass}

View File

@@ -1,139 +0,0 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { sanitizeUrl } from "@braintree/sanitize-url";
import {
createSearchParam,
extractSearchParamsObject,
} from "../../src/common/url/search-params";
import "../../src/layouts/hass-error-screen";
import {
ParamType,
Redirect,
Redirects,
} from "../../src/panels/my/ha-panel-my";
import { navigate } from "../../src/common/navigate";
import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
supervisor_info: {
redirect: "/hassio/system",
},
supervisor_snapshots: {
redirect: "/hassio/snapshots",
},
supervisor_store: {
redirect: "/hassio/store",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")
class HassioMyRedirect extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@internalProperty() public _error?: TemplateResult | string;
connectedCallback() {
super.connectedCallback();
const path = this.route.path.substr(1);
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = this.supervisor.localize(
"my.not_supported",
"link",
html`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>
${this.supervisor.localize("my.faq_link")}
</a>`
);
return;
}
let url: string;
try {
url = this._createRedirectUrl(redirect);
} catch (err) {
this._error = this.supervisor.localize("my.error");
return;
}
navigate(this, url, true);
}
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
return html``;
}
private _createRedirectUrl(redirect: Redirect): string {
const params = this._createRedirectParams(redirect);
return `${redirect.redirect}${params}`;
}
private _createRedirectParams(redirect: Redirect): string {
const params = extractSearchParamsObject();
if (!redirect.params && !Object.keys(params).length) {
return "";
}
const resultParams = {};
Object.entries(redirect.params || {}).forEach(([key, type]) => {
if (!params[key] || !this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
});
return `?${createSearchParam(resultParams)}`;
}
private _checkParamType(type: ParamType, value: string) {
if (type === "string") {
return true;
}
if (type === "url") {
return value && value === sanitizeUrl(value);
}
return false;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-my-redirect": HassioMyRedirect;
}
}

View File

@@ -7,10 +7,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import {
Supervisor,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@@ -25,17 +22,6 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}

View File

@@ -23,7 +23,7 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._redirectIngress(),
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
@@ -41,42 +41,32 @@ class HassioRouter extends HassRouterPage {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
_my_redirect: {
tag: "hassio-my-redirect",
load: () => import("./hassio-my-redirect"),
},
},
};
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
el.hass = this.hass;
el.supervisor = this.supervisor;
el.narrow = this.narrow;
el.route = route;
if (el.localName === "hassio-ingress-view") {
el.ingressPanel = this.panel.config && this.panel.config.ingress;
} else {
el.supervisor = this.supervisor;
}
}
private async _redirectIngress() {
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
this._redirectIngress(this.panel.config.ingress);
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
}
declare global {

View File

@@ -3,22 +3,22 @@ import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
translationKey: "panel.dashboard",
name: "Dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
translationKey: "panel.store",
name: "Add-on Store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
translationKey: "panel.snapshots",
name: "Snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
name: "System",
path: `/hassio/system`,
iconPath: mdiCogs,
},

View File

@@ -55,8 +55,8 @@ import { hassioStyle } from "../resources/hassio-style";
interface CheckboxItem {
slug: string;
name: string;
checked: boolean;
name?: string;
}
@customElement("hassio-snapshots")
@@ -84,12 +84,13 @@ class HassioSnapshots extends LitElement {
@internalProperty() private _folderList: CheckboxItem[] = [
{
slug: "homeassistant",
name: "Home Assistant configuration",
checked: true,
},
{ slug: "ssl", checked: true },
{ slug: "share", checked: true },
{ slug: "media", checked: true },
{ slug: "addons/local", checked: true },
{ slug: "ssl", name: "SSL", checked: true },
{ slug: "share", name: "Share", checked: true },
{ slug: "media", name: "Media", checked: true },
{ slug: "addons/local", name: "Local add-ons", checked: true },
];
@internalProperty() private _error = "";
@@ -103,16 +104,13 @@ class HassioSnapshots extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">
${this.supervisor.localize("panel.snapshots")}
</span>
<span slot="header">Snapshots</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -122,50 +120,50 @@ class HassioSnapshots extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
${this.supervisor.localize("common.reload")}
Reload
</mwc-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<mwc-list-item>
${this.supervisor.localize("snapshot.upload_snapshot")}
Upload snapshot
</mwc-list-item>`
: ""}
</ha-button-menu>
<div class="content">
<h1>
${this.supervisor.localize("snapshot.create_snapshot")}
Create Snapshot
</h1>
<p class="description">
${this.supervisor.localize("snapshot.description")}
Snapshots allow you to easily backup and restore all data of your
Home Assistant instance.
</p>
<div class="card-group">
<ha-card>
<div class="card-content">
<paper-input
autofocus
.label=${this.supervisor.localize("snapshot.name")}
label="Name"
name="snapshotName"
.value=${this._snapshotName}
@value-changed=${this._handleTextValueChanged}
></paper-input>
${this.supervisor.localize("snapshot.type")}:
Type:
<paper-radio-group
name="snapshotType"
type="${this.supervisor.localize("snapshot.type")}"
.selected=${this._snapshotType}
@selected-changed=${this._handleRadioValueChanged}
>
<paper-radio-button name="full">
${this.supervisor.localize("snapshot.full_snapshot")}
Full snapshot
</paper-radio-button>
<paper-radio-button name="partial">
${this.supervisor.localize("snapshot.partial_snapshot")}
Partial snapshot
</paper-radio-button>
</paper-radio-group>
${this._snapshotType === "full"
? undefined
: html`
${this.supervisor.localize("snapshot.folders")}:
Folders:
${this._folderList.map(
(folder, idx) => html`
<paper-checkbox
@@ -173,13 +171,11 @@ class HassioSnapshots extends LitElement {
.checked=${folder.checked}
@checked-changed=${this._folderChecked}
>
${this.supervisor.localize(
`snapshot.folder.${folder.slug}`
)}
${folder.name}
</paper-checkbox>
`
)}
${this.supervisor.localize("snapshot.addons")}:
Add-ons:
${this._addonList.map(
(addon, idx) => html`
<paper-checkbox
@@ -192,18 +188,18 @@ class HassioSnapshots extends LitElement {
`
)}
`}
${this.supervisor.localize("snapshot.security")}:
Security:
<paper-checkbox
name="snapshotHasPassword"
.checked=${this._snapshotHasPassword}
@checked-changed=${this._handleCheckboxValueChanged}
>
${this.supervisor.localize("snapshot.password_protection")}
Password protection
</paper-checkbox>
${this._snapshotHasPassword
? html`
<paper-input
.label=${this.supervisor.localize("snapshot.password")}
label="Password"
type="password"
name="snapshotPassword"
.value=${this._snapshotPassword}
@@ -218,22 +214,18 @@ class HassioSnapshots extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._createSnapshot}
.title=${this.supervisor.info.state !== "running"
? this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
)
: ""}
title="${this.supervisor.info.state !== "running"
? `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`
: ""}"
.disabled=${this.supervisor.info.state !== "running"}
>
${this.supervisor.localize("snapshot.create")}
Create
</ha-progress-button>
</div>
</ha-card>
</div>
<h1>${this.supervisor.localize("snapshot.available_snapshots")}</h1>
<h1>Available Snapshots</h1>
<div class="card-group">
${this._snapshots === undefined
? undefined
@@ -241,7 +233,7 @@ class HassioSnapshots extends LitElement {
? html`
<ha-card>
<div class="card-content">
${this.supervisor.localize("snapshot.no_snapshots")}
You don't have any snapshots yet.
</div>
</ha-card>
`
@@ -342,12 +334,8 @@ class HassioSnapshots extends LitElement {
private async _createSnapshot(ev: CustomEvent): Promise<void> {
if (this.supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: this.supervisor.localize("snapshot.could_not_create"),
text: this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
),
title: "Could not create snapshot",
text: `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
});
}
const button = ev.currentTarget as any;
@@ -355,7 +343,7 @@ class HassioSnapshots extends LitElement {
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = this.supervisor.localize("snapshot.enter_password");
this._error = "Please enter a password.";
button.progress = false;
return;
}
@@ -404,9 +392,7 @@ class HassioSnapshots extends LitElement {
private _computeDetails(snapshot: HassioSnapshot) {
const type =
snapshot.type === "full"
? this.supervisor.localize("snapshot.full_snapshot")
: this.supervisor.localize("snapshot.partial_snapshot");
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
return snapshot.protected ? `${type}, password protected` : type;
}

View File

@@ -1,14 +1,4 @@
import { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
internalProperty,
LitElement,
property,
PropertyValues,
} from "lit-element";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import { LitElement, property, PropertyValues } from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
@@ -20,202 +10,60 @@ import {
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
Supervisor,
SupervisorObject,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@internalProperty() private _collections: Record<
string,
Collection<unknown>
> = {};
@internalProperty() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
});
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (
oldHass !== undefined &&
oldHass.language !== undefined &&
oldHass.language !== this.hass.language
) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
if (changedProperties.has("_collections")) {
if (this._collections) {
const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[
collection
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
);
}
}
}
}
}
@property({ attribute: false }) public supervisor?: Supervisor;
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor, ...obj };
this.supervisor = { ...this.supervisor!, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (
this._language !== this.hass.language &&
this.hass.language !== undefined
) {
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(
null,
this._language,
"/api/hassio/app/static/translations"
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[collection].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [collection]: response.data });
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
const [
supervisor,
host,
core,
info,
os,
network,
resolution,
] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
]);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._collections[collection].refresh();
} else {
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
collection,
supervisorCollection[collection]
);
}
});
Object.keys(this._collections).forEach((collection) => {
if (
this.supervisor === undefined ||
this.supervisor[collection] === undefined
) {
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this._updateSupervisor({
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
});
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
this.supervisor = {
supervisor,
host,
core,
info,
os,
network,
resolution,
};
}
}

View File

@@ -10,7 +10,6 @@ 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-button-menu";
import "../../../src/components/ha-card";
@@ -30,7 +29,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -44,11 +42,11 @@ class HassioCoreInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: this.supervisor.localize("system.core.cpu_usage"),
description: "Core CPU Usage",
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("system.core.ram_usage"),
description: "Core RAM Usage",
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -62,7 +60,7 @@ class HassioCoreInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.version")}
Version
</span>
<span slot="description">
core-${this.supervisor.core.version}
@@ -70,7 +68,7 @@ class HassioCoreInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
Newest Version
</span>
<span slot="description">
core-${this.supervisor.core.version_latest}
@@ -78,10 +76,10 @@ class HassioCoreInfo extends LitElement {
${this.supervisor.core.update_available
? html`
<ha-progress-button
.title=${this.supervisor.localize("common.update")}
title="Update the core"
@click=${this._coreUpdate}
>
${this.supervisor.localize("common.update")}
Update
</ha-progress-button>
`
: ""}
@@ -105,13 +103,9 @@ class HassioCoreInfo extends LitElement {
slot="primaryAction"
class="warning"
@click=${this._coreRestart}
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Core"
)}
title="Restart Home Assistant Core"
>
${this.supervisor.localize("common.restart_name", "name", "Core")}
Restart Core
</ha-progress-button>
</div>
</ha-card>
@@ -131,18 +125,10 @@ class HassioCoreInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Home Assistant Core"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Home Assistant Core"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
title: "Restart Home Assistant Core",
text: "Are you sure you want to restart Home Assistant Core",
confirmText: "restart",
dismissText: "cancel",
});
if (!confirmed) {
@@ -153,40 +139,41 @@ class HassioCoreInfo extends LitElement {
try {
await restartCore(this.hass);
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Home AssistantCore"
),
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to restart Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _coreUpdate(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => await this._updateCore(),
});
}
private async _coreUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
const confirmed = await showConfirmationDialog(this, {
title: "Update Home Assistant Core",
text: `Are you sure you want to update Home Assistant Core to version ${this.supervisor.core.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateCore(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
static get styles(): CSSResult[] {

View File

@@ -13,7 +13,6 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
@@ -21,12 +20,13 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoreSupervisorError,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
fetchHassioHostInfo,
rebootHost,
shutdownHost,
updateOS,
@@ -65,7 +65,7 @@ class HassioHostInfo extends LitElement {
const metrics = [
{
description: this.supervisor.localize("system.host.used_space"),
description: "Used Space",
value: this._getUsedSpace(
this.supervisor.host.disk_used,
this.supervisor.host.disk_total
@@ -80,13 +80,14 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.hostname")}
Hostname
</span>
<span slot="description">
${this.supervisor.host.hostname}
</span>
<mwc-button
.label=${this.supervisor.localize("system.host.change")}
title="Change the hostname"
label="Change"
@click=${this._changeHostnameClicked}
>
</mwc-button>
@@ -95,13 +96,14 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.ip_address")}
IP Address
</span>
<span slot="description">
${primaryIpAddress}
</span>
<mwc-button
.label=${this.supervisor.localize("system.host.change")}
title="Change the network"
label="Change"
@click=${this._changeNetworkClicked}
>
</mwc-button>
@@ -110,15 +112,18 @@ class HassioHostInfo extends LitElement {
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.operating_system")}
Operating System
</span>
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${this.supervisor.os.update_available
? html`
<ha-progress-button @click=${this._osUpdate}>
${this.supervisor.localize("commmon.update")}
<ha-progress-button
title="Update the host OS"
@click=${this._osUpdate}
>
Update
</ha-progress-button>
`
: ""}
@@ -126,7 +131,7 @@ class HassioHostInfo extends LitElement {
${!this.supervisor.host.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.docker_version")}
Docker version
</span>
<span slot="description">
${this.supervisor.info.docker}
@@ -136,7 +141,7 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.deployment
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.deployment")}
Deployment
</span>
<span slot="description">
${this.supervisor.host.deployment}
@@ -149,13 +154,11 @@ class HassioHostInfo extends LitElement {
this.supervisor.host.disk_life_time >= 10
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize(
"system.host.emmc_lifetime_used"
)}
eMMC Lifetime Used
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time} %
${this.supervisor.host.disk_life_time - 10}% -
${this.supervisor.host.disk_life_time}%
</span>
</ha-settings-row>`
: ""}
@@ -174,18 +177,23 @@ class HassioHostInfo extends LitElement {
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
? html`
<ha-progress-button class="warning" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")}
<ha-progress-button
title="Reboot the host OS"
class="warning"
@click=${this._hostReboot}
>
Reboot Host
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
? html`
<ha-progress-button
title="Shutdown the host OS"
class="warning"
@click=${this._hostShutdown}
>
${this.supervisor.localize("system.host.shutdown_host")}
Shutdown Host
</ha-progress-button>
`
: ""}
@@ -197,12 +205,14 @@ class HassioHostInfo extends LitElement {
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
${this.supervisor.localize("system.host.hardware")}
<mwc-list-item title="Show a list of hardware">
Hardware
</mwc-list-item>
${this.supervisor.host.features.includes("hassos")
? html`<mwc-list-item>
${this.supervisor.localize("system.host.import_from_usb")}
? html`<mwc-list-item
title="Load HassOS configs or updates from USB"
>
Import from USB
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -241,14 +251,12 @@ class HassioHostInfo extends LitElement {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
title: "Failed to get hardware list",
text: extractApiErrorMessage(err),
});
}
@@ -259,10 +267,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.reboot_host"),
text: this.supervisor.localize("system.host.confirm_reboot"),
confirmText: this.supervisor.localize("system.host.reboot_host"),
dismissText: this.supervisor.localize("common.cancel"),
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
@@ -274,9 +282,9 @@ class HassioHostInfo extends LitElement {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"),
title: "Failed to reboot",
text: extractApiErrorMessage(err),
});
}
@@ -289,10 +297,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.shutdown_host"),
text: this.supervisor.localize("system.host.confirm_shutdown"),
confirmText: this.supervisor.localize("system.host.shutdown_host"),
dismissText: this.supervisor.localize("common.cancel"),
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
@@ -304,9 +312,9 @@ class HassioHostInfo extends LitElement {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"),
title: "Failed to shutdown",
text: extractApiErrorMessage(err),
});
}
@@ -319,19 +327,9 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
@@ -342,25 +340,18 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
supervisor: this.supervisor,
network: this.supervisor.network!,
loadData: () => this._loadData(),
});
}
@@ -368,22 +359,20 @@ class HassioHostInfo extends LitElement {
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, {
title: this.supervisor.localize("system.host.change_hostname"),
inputLabel: this.supervisor.localize("system.host.new_hostname"),
title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
confirmText: this.supervisor.localize("common.update"),
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
title: "Setting hostname failed",
text: extractApiErrorMessage(err),
});
}
@@ -393,28 +382,19 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_import_from_usb"
),
title: "Failed to import from USB",
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
}
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
}
static get styles(): CSSResult[] {

View File

@@ -8,7 +8,6 @@ import {
property,
TemplateResult,
} from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
@@ -20,6 +19,7 @@ import {
HassioStats,
} from "../../../src/data/hassio/common";
import {
fetchHassioSupervisorInfo,
reloadSupervisor,
restartSupervisor,
setSupervisorOption,
@@ -38,27 +38,54 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = {
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
const UNSUPPORTED_REASON = {
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",
},
job_conditions: {
title: "Ignored job conditions",
url: "/more-info/unsupported/job_conditions",
},
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" },
};
const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
const UNHEALTHY_REASON = {
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
supervisor: {
title: "Supervisor was not able to update",
url: "/more-info/unhealthy/supervisor",
},
setup: {
title: "Setup of the Supervisor failed",
url: "/more-info/unhealthy/setup",
},
docker: {
title: "The Docker environment is not working properly",
url: "/more-info/unhealthy/docker",
},
};
@customElement("hassio-supervisor-info")
@@ -72,11 +99,11 @@ class HassioSupervisorInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: this.supervisor.localize("system.supervisor.cpu_usage"),
description: "Supervisor CPU Usage",
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("system.supervisor.ram_usage"),
description: "Supervisor RAM Usage",
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -89,7 +116,7 @@ class HassioSupervisorInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.version")}
Version
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version}
@@ -97,7 +124,7 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
Newest Version
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version_latest}
@@ -105,19 +132,17 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.supervisor.update_available
? html`
<ha-progress-button
.title=${this.supervisor.localize(
"system.supervisor.update_supervisor"
)}
title="Update the supervisor"
@click=${this._supervisorUpdate}
>
${this.supervisor.localize("common.update")}
Update
</ha-progress-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.supervisor.channel")}
Channel
</span>
<span slot="description">
${this.supervisor.supervisor.channel}
@@ -126,65 +151,49 @@ class HassioSupervisorInfo extends LitElement {
? html`
<ha-progress-button
@click=${this._toggleBeta}
.title=${this.supervisor.localize(
"system.supervisor.leave_beta_description"
)}
title="Get stable updates for Home Assistant, supervisor and host"
>
${this.supervisor.localize(
"system.supervisor.leave_beta_action"
)}
Leave beta channel
</ha-progress-button>
`
: this.supervisor.supervisor.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
.title=${this.supervisor.localize(
"system.supervisor.join_beta_description"
)}
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>
${this.supervisor.localize(
"system.supervisor.join_beta_action"
)}
Join beta channel
</ha-progress-button>
`
: ""}
</ha-settings-row>
${this.supervisor.supervisor.supported
? !atLeastVersion(this.hass.config.version, 2021, 4)
? html` <ha-settings-row three-line>
<span slot="heading">
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: ""
? html` <ha-settings-row three-line>
<span slot="heading">
Share Diagnostics
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
<button
class="link"
title="Show more information about this"
@click=${this._diagnosticsInformationDialog}
>
Learn more
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: html`<div class="error">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
You are running an unsupported installation.
<button
class="link"
.title=${this.supervisor.localize("common.learn_more")}
title="Learn more about how you can make your system compliant"
@click=${this._unsupportedDialog}
>
Learn more
@@ -192,12 +201,10 @@ class HassioSupervisorInfo extends LitElement {
</div>`}
${!this.supervisor.supervisor.healthy
? html`<div class="error">
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
Your installation is running in an unhealthy state.
<button
class="link"
.title=${this.supervisor.localize("common.learn_more")}
title="Learn more about why your system is marked as unhealthy"
@click=${this._unhealthyDialog}
>
Learn more
@@ -221,26 +228,16 @@ class HassioSupervisorInfo extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
.title=${this.supervisor.localize(
"system.supervisor.reload_supervisor"
)}
title="Reload parts of the Supervisor"
>
${this.supervisor.localize("system.supervisor.reload_supervisor")}
Reload Supervisor
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
title="Restart the Supervisor"
>
${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
Restart Supervisor
</ha-progress-button>
</div>
</ha-card>
@@ -261,25 +258,23 @@ class HassioSupervisorInfo extends LitElement {
if (this.supervisor.supervisor.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.supervisor.warning"),
text: html`${this.supervisor.localize("system.supervisor.beta_warning")}
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
${this.supervisor.localize("system.supervisor.beta_backup")}
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
${this.supervisor.localize("system.supervisor.beta_release_items")}
<ul>
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
</ul>
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
confirmText: this.supervisor.localize(
"system.supervisor.join_beta_action"
),
dismissText: this.supervisor.localize("common.cancel"),
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
@@ -297,9 +292,7 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
} finally {
@@ -315,7 +308,7 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
@@ -325,9 +318,8 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
@@ -335,18 +327,10 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Supervisor"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
title: "Restart the Supervisor",
text: "Are you sure you want to restart the Supervisor",
confirmText: "restart",
dismissText: "cancel",
});
if (!confirmed) {
@@ -358,11 +342,7 @@ class HassioSupervisorInfo extends LitElement {
await restartSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Supervisor"
),
title: "Failed to restart the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
@@ -375,20 +355,10 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
@@ -398,16 +368,9 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
title: "Failed to update the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
@@ -417,41 +380,40 @@ class HassioSupervisorInfo extends LitElement {
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.share_diagonstics_title"
),
text: this.supervisor.localize(
"system.supervisor.share_diagonstics_description",
"line_break",
html`<br /><br />`
),
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _unsupportedDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.unsupported_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unsupported_description"
)} <br /><br />
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>
${this.supervisor.resolution.unsupported.map(
(reason) => html`
(issue) => html`
<li>
${UNSUPPORTED_REASON_URL[reason]
${UNSUPPORTED_REASON[issue]
? html`<a
href="${documentationUrl(
this.hass,
UNSUPPORTED_REASON_URL[reason]
UNSUPPORTED_REASON[issue].url
)}"
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
${UNSUPPORTED_REASON[issue].title}
</a>`
: reason}
: issue}
</li>
`
)}
@@ -461,28 +423,26 @@ class HassioSupervisorInfo extends LitElement {
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unhealthy_description"
)} <br /><br />
title: "Your installation is unhealthy",
text: html`Running an unhealthy installation will cause issues. 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>
${this.supervisor.resolution.unhealthy.map(
(reason) => html`
(issue) => html`
<li>
${UNHEALTHY_REASON_URL[reason]
${UNHEALTHY_REASON[issue]
? html`<a
href="${documentationUrl(
this.hass,
UNHEALTHY_REASON_URL[reason]
UNHEALTHY_REASON[issue].url
)}"
target="_blank"
rel="noreferrer"
>
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
${UNHEALTHY_REASON[issue].title}
</a>`
: reason}
: issue}
</li>
`
)}
@@ -498,9 +458,7 @@ class HassioSupervisorInfo extends LitElement {
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}

View File

@@ -16,7 +16,6 @@ import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
@@ -59,8 +58,6 @@ const logProviders: LogProvider[] = [
class HassioSupervisorLog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _error?: string;
@internalProperty() private _selectedLogProvider = "supervisor";
@@ -79,7 +76,7 @@ class HassioSupervisorLog extends LitElement {
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
.label=${this.supervisor.localize("system.log.log_provider")}
label="Log Provider"
@iron-select=${this._setLogProvider}
>
<paper-listbox
@@ -89,9 +86,9 @@ class HassioSupervisorLog extends LitElement {
>
${logProviders.map((provider) => {
return html`
<paper-item provider=${provider.key}>
${provider.name}
</paper-item>
<paper-item provider=${provider.key}
>${provider.name}</paper-item
>
`;
})}
</paper-listbox>
@@ -101,13 +98,14 @@ class HassioSupervisorLog extends LitElement {
<div class="card-content" id="content">
${this._content
? html`<hassio-ansi-to-html .content=${this._content}>
</hassio-ansi-to-html>`
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
Refresh
</ha-progress-button>
</div>
</ha-card>
@@ -136,13 +134,9 @@ class HassioSupervisorLog extends LitElement {
this._selectedLogProvider
);
} catch (err) {
this._error = this.supervisor.localize(
"system.log.get_logs",
"provider",
this._selectedLogProvider,
"error",
extractApiErrorMessage(err)
);
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
}
}

View File

@@ -32,16 +32,13 @@ class HassioSystem extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">
${this.supervisor.localize("panel.system")}
</span>
<span slot="header">System</span>
<div class="content">
<div class="card-group">
<hassio-core-info
@@ -57,10 +54,7 @@ class HassioSystem extends LitElement {
.supervisor=${this.supervisor}
></hassio-host-info>
</div>
<hassio-supervisor-log
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-supervisor-log>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
`;

View File

@@ -23,17 +23,6 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.0",
"@codemirror/history": "^0.18.0",
"@codemirror/legacy-modes": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0",
"@codemirror/search": "^0.18.0",
"@codemirror/state": "^0.18.0",
"@codemirror/stream-parser": "^0.18.0",
"@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@@ -57,8 +46,8 @@
"@material/mwc-tab": "^0.20.0",
"@material/mwc-tab-bar": "^0.20.0",
"@material/top-app-bar": "=9.0.0-canary.1c156d69d.0",
"@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -91,6 +80,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.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",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
@@ -100,6 +91,7 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"core-js": "^3.6.5",
"cropperjs": "^1.5.7",
@@ -108,8 +100,8 @@
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.1",
"home-assistant-js-websocket": "^5.9.0",
"hls.js": "^0.13.2",
"home-assistant-js-websocket": "^5.4.1",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -118,7 +110,7 @@
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"lit-virtualizer": "^0.4.2",
"marked": "2.0.0",
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "3.2.1-alpha.1",
@@ -131,19 +123,16 @@
"sortablejs": "^1.10.2",
"superstruct": "^0.10.13",
"tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-core": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3",
"xss": "^1.0.6"
},
"devDependencies": {
@@ -167,15 +156,15 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.2.2",
"@types/marked": "^1.1.0",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
@@ -188,7 +177,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -224,17 +213,17 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.1",
"terser-webpack-plugin": "^5.0.0",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.2.4",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0",
"workbox-build": "^6.1.5"
"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"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -12,5 +12,5 @@ yarn install
script/build_frontend
rm -rf dist
python3 setup.py -q sdist
python3 setup.py sdist
python3 -m twine upload dist/* --skip-existing

View File

@@ -10,10 +10,10 @@ function patch(version) {
function today() {
const now = new Date();
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(
2,
"0"
)}${String(now.getUTCDate()).padStart(2, "0")}.0`;
)}${String(now.getDate()).padStart(2, "0")}.0`;
}
function auto(version) {

View File

@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210429.0",
version="20210208.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache-2.0",
license="Apache License 2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,

View File

@@ -8,7 +8,6 @@ import {
PropertyValues,
} from "lit-element";
import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import {
AuthProvider,
@@ -117,20 +116,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
this._fetchDiscoveryInfo();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
if (!this.redirectUri) {
return;
}

View File

@@ -62,7 +62,7 @@ export const ensureConnectedCastSession = (cast: CastManager, auth: Auth) => {
return undefined;
}
return new Promise<void>((resolve) => {
return new Promise((resolve) => {
const unsub = cast.addEventListener("connection-changed", () => {
if (cast.castConnectedToOurHass) {
unsub();

View File

@@ -102,18 +102,3 @@ export const lab2hex = (lab: [number, number, number]): string => {
const rgb = lab2rgb(lab);
return rgb2hex(rgb);
};
export const rgb2hsv = (
rgb: [number, number, number]
): [number, number, number] => {
const [r, g, b] = rgb;
const v = Math.max(r, g, b);
const c = v - Math.min(r, g, b);
const h =
c && (v === r ? (g - b) / c : v === g ? 2 + (b - r) / c : 4 + (r - g) / c);
return [60 * (h < 0 ? h + 6 : h), v && c / v, v];
};
export const rgb2hs = (rgb: [number, number, number]): [number, number] => {
return rgb2hsv(rgb).slice(0, 2) as [number, number];
};

View File

@@ -1,20 +1,11 @@
export const atLeastVersion = (
version: string,
major: number,
minor: number,
patch?: number
minor: number
): boolean => {
const [haMajor, haMinor, haPatch] = version.split(".", 3);
const [haMajor, haMinor] = version.split(".", 2);
return (
Number(haMajor) > major ||
(Number(haMajor) === major &&
(patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(patch !== undefined &&
Number(haMajor) === major &&
Number(haMinor) === minor &&
Number(haPatch) >= patch)
(Number(haMajor) === major && Number(haMinor) >= minor)
);
};

View File

@@ -56,8 +56,6 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
@@ -105,7 +103,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"lock",
"media_player",
"person",
"remote",
"script",
"sun",
"timer",

View File

@@ -1,10 +1,9 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
year: "numeric",
month: "long",
day: "numeric",
@@ -12,8 +11,8 @@ export const formatDate = toLocaleDateStringSupportsOptions
: (dateObj: Date) => format(dateObj, "longDate");
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
weekday: "long",
month: "short",
day: "numeric",

View File

@@ -1,10 +1,9 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support";
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
month: "long",
day: "numeric",
@@ -14,8 +13,8 @@ export const formatDateTime = toLocaleStringSupportsOptions
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
month: "long",
day: "numeric",

View File

@@ -1,18 +1,17 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
@@ -20,8 +19,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
: (dateObj: Date) => format(dateObj, "mediumTime");
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
weekday: "long",
hour: "numeric",
minute: "2-digit",

View File

@@ -70,18 +70,13 @@ export const applyThemesOnElement = (
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
}
if (selectedTheme && themes.themes[selectedTheme]) {
themeRules = themes.themes[selectedTheme];
}
if (!element._themes?.keys && !Object.keys(themeRules).length) {
if (!element._themes && !Object.keys(themeRules).length) {
// No styles to reset, and no styles to set
return;
}
@@ -92,8 +87,8 @@ export const applyThemesOnElement = (
: undefined;
// Add previous set keys to reset them, and new theme
const styles = { ...element._themes?.keys, ...newTheme?.styles };
element._themes = { cacheKey, keys: newTheme?.keys };
const styles = { ...element._themes, ...newTheme?.styles };
element._themes = newTheme?.keys;
// Set and/or reset styles
if (element.updateStyles) {

View File

@@ -1,10 +1,6 @@
type NonUndefined<T> = T extends undefined ? never : T;
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
export const ensureArray = (value?: any) => {
if (!value || Array.isArray(value)) {
return value;
}
return [value];
}
};

View File

@@ -8,19 +8,12 @@ export const batteryIcon = (
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
return "hass:battery-unknown";
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

@@ -1,6 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendTranslationData } from "../../data/translation";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
@@ -11,7 +10,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendTranslationData,
language: string,
state?: string
): string => {
const compareState = state !== undefined ? state : stateObj.state;
@@ -21,7 +20,7 @@ export const computeStateDisplay = (
}
if (stateObj.attributes.unit_of_measurement) {
return `${formatNumber(compareState, locale)} ${
return `${formatNumber(compareState, language)} ${
stateObj.attributes.unit_of_measurement
}`;
}
@@ -36,7 +35,7 @@ export const computeStateDisplay = (
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale);
return formatDate(date, language);
}
if (!stateObj.attributes.has_date) {
const now = new Date();
@@ -49,7 +48,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatTime(date, locale);
return formatTime(date, language);
}
date = new Date(
@@ -59,7 +58,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
return formatDateTime(date, language);
}
if (domain === "humidifier") {
@@ -68,13 +67,8 @@ export const computeStateDisplay = (
}
}
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
if (
domain === "counter" ||
domain === "number" ||
domain === "input_number"
) {
return formatNumber(compareState, locale);
if (domain === "counter") {
return formatNumber(compareState, language);
}
return (

View File

@@ -12,24 +12,16 @@ declare global {
export const navigate = (_node: any, path: string, replace = false) => {
if (__DEMO__) {
if (replace) {
top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
`${top.location.pathname}#${path}`
);
history.replaceState(null, "", `${location.pathname}#${path}`);
} else {
top.location.hash = path;
window.location.hash = path;
}
} else if (replace) {
top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
path
);
history.replaceState(null, "", path);
} else {
top.history.pushState(null, "", path);
history.pushState(null, "", path);
}
fireEvent(top, "location-changed", {
fireEvent(window, "location-changed", {
replace,
});
};

View File

@@ -34,12 +34,14 @@ const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
}
for (let i = 0; i <= _maxLen; i++) {
table.push(row.slice(0));
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
}
return table;
}
@@ -48,7 +50,7 @@ function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.codePointAt(index);
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
@@ -60,16 +62,8 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
}
}
@@ -98,15 +92,10 @@ function isPatternInWord(
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number,
fillMinWordPosArr = false
wordLen: number
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
@@ -115,22 +104,42 @@ function isPatternInWord(
}
enum Arrow {
Diag = 1,
Left = 2,
LeftLeft = 3,
Top = 0b1,
Diag = 0b10,
Left = 0b100,
}
/**
* An array representating a fuzzy match.
*
* A tuple of three values.
* 0. the score
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
*/
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<number>;
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,
@@ -141,6 +150,7 @@ export function fuzzyScore(
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;
@@ -162,30 +172,18 @@ export function fuzzyScore(
patternLen,
wordLow,
wordStart,
wordLen,
true
wordLen
)
) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(
patternLen,
wordLen,
patternStart,
wordStart,
patternLow,
wordLow
);
let row = 1;
let column = 1;
let patternPos = patternStart;
let wordPos = wordStart;
const hasStrongFirstMatch = [false];
let hasStrongFirstMatch = false;
// There will be a match, fill in tables
for (
@@ -193,146 +191,83 @@ export function fuzzyScore(
patternPos < patternLen;
row++, patternPos++
) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos =
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
for (
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < nextMaxWordMatchPos;
column = 1, wordPos = wordStart;
wordPos < wordLen;
column++, wordPos++
) {
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
const score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos,
wordLen,
wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
}
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
globals._scores[row][column] = score;
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
: 0; // penalty for a gap start
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;
const canComeLeftLeft =
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (
canComeLeftLeft &&
(!canComeLeft || leftLeftScore >= leftScore) &&
(!canComeDiag || leftLeftScore >= diagScore)
) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][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 {
throw new Error(`not possible`);
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart);
printTables(pattern, patternStart, word, wordStart, globals);
}
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
return undefined;
}
row--;
column--;
globals._matchesCount = 0;
globals._topScore = -100;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn -= 2;
} else if (arrow === Arrow.Left) {
diagColumn -= 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
_findAllMatches2(
row - 1,
column - 1,
patternLen === wordLen ? 1 : 0,
0,
false,
globals
);
if (globals._matchesCount === 0) {
return undefined;
}
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
return [globals._topScore, globals._topMatch2, wordStart];
}
function _doScore(
@@ -342,81 +277,50 @@ function _doScore(
patternStart: number,
word: string,
wordLow: string,
wordPos: number,
wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
wordPos: number
) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
return -1;
}
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (
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`
// ^^ ^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
score = 5;
} else if (
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
score = 5;
isGapLocation = true;
return 5;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation =
isUpperCaseAtPos(wordPos, word, wordLow) ||
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) {
// first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
return 1;
}
function printTable(
@@ -456,96 +360,104 @@ function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number
wordStart: number,
globals: FilterGlobals
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _arrows = <Arrow[][]>initTable();
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
function _fillInMaxWordMatchPos(
patternLen: number,
wordLen: number,
patternStart: number,
wordStart: number,
patternLow: string,
wordLow: string
) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
export interface FuzzyScorer {
(
pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
}
/**
* A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
*/
export function isEmojiImprecise(x: number): boolean {
return (
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
x === 8986 ||
x === 8987 ||
x === 9200 ||
x === 9203 ||
(x >= 9728 && x <= 10175) ||
x === 11088 ||
x === 11093 ||
(x >= 127744 && x <= 128591) ||
(x >= 128640 && x <= 128764) ||
(x >= 128992 && x <= 129003) ||
(x >= 129280 && x <= 129535) ||
(x >= 129648 && x <= 129750)
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

View File

@@ -10,13 +10,10 @@ import { fuzzyScore } from "./filter";
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
let topScore = 0;
for (const word of item.strings) {
for (const word of words) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
@@ -31,39 +28,22 @@ export const fuzzySequentialMatch = (
continue;
}
// The VS Code implementation of filter returns a 0 for a weak match.
// But if .filter() sees a "0", it considers that a failed match and will remove it.
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] === 0 ? 1 : scores[0];
// 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;
}
}
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore;
};
/**
* An interface that objects must extend in order to use the fuzzy sequence matcher
*
* @param {number} score - A number representing the existence and strength of a match.
* - `< 0` means a good match that starts in the middle of the string
* - `> 0` means a good match that starts at the beginning of the string
* - `0` means just barely a match
* - `undefined` means not a match
*
* @param {string} strings - Array of strings (aliases) representing the item. The filter string will be compared against each of these for a match.
*
*/
export interface ScorableTextItem {
score?: number;
strings: string[];
text: string;
altText?: string;
}
type FuzzyFilterSort = <T extends ScorableTextItem>(
@@ -74,10 +54,12 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = fuzzySequentialMatch(filter, item);
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item;
})
.filter((item) => item.score !== undefined)
.filter((item) => item.score !== undefined && item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);

View File

@@ -1,36 +1,14 @@
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
/**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
* 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 locale The user-selected language and number format, from `hass.locale`
* @param options Intl.NumberFormatOptions to use
* @param language The language to use when formatting the number
*/
export const formatNumber = (
num: string | number,
locale?: FrontendTranslationData,
language: string,
options?: Intl.NumberFormatOptions
): string => {
let format: string | string[] | undefined;
switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
@@ -38,25 +16,11 @@ export const formatNumber = (
return typeof input === "number" && isNaN(input);
};
if (
!Number.isNaN(Number(num)) &&
Intl &&
locale?.number_format !== NumberFormat.none
) {
try {
return new Intl.NumberFormat(
format,
getDefaultFormatOptions(num, options)
).format(Number(num));
} catch (error) {
// Don't fail when using "TEST" language
// eslint-disable-next-line no-console
console.error(error);
return new Intl.NumberFormat(
undefined,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(
language,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
return num.toString();
};

View File

@@ -1,18 +0,0 @@
const isTemplateRegex = new RegExp("{%|{{");
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);
export const hasTemplate = (value: unknown): boolean => {
if (!value) {
return false;
}
if (typeof value === "string") {
return isTemplate(value);
}
if (typeof value === "object") {
const values = Array.isArray(value) ? value : Object.values(value!);
return values.some((val) => val && hasTemplate(val));
}
return false;
};

View File

@@ -1,2 +0,0 @@
export const strStartsWith = (value: string, search: string) =>
value.substring(0, search.length) === search;

View File

@@ -15,7 +15,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],

View File

@@ -1,5 +0,0 @@
export const constructUrlCurrentPath = (searchParams: string): string => {
const base = window.location.pathname;
// Prevent trailing "?" if no parameters exist
return searchParams ? base + "?" + searchParams : base;
};

View File

@@ -19,17 +19,3 @@ export const createSearchParam = (params: Record<string, string>): string => {
});
return urlParams.toString();
};
export const addSearchParam = (params: Record<string, string>): string => {
const urlParams = new URLSearchParams(window.location.search);
Object.entries(params).forEach(([key, value]) => {
urlParams.set(key, value);
});
return urlParams.toString();
};
export const removeSearchParam = (param: string): string => {
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete(param);
return urlParams.toString();
};

View File

@@ -1,4 +1,4 @@
export const afterNextRender = (cb: (value: unknown) => void): void => {
export const afterNextRender = (cb: () => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0));
};

View File

@@ -0,0 +1,65 @@
import { html, LitElement } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "./ha-progress-button";
class HaCallApiButton extends LitElement {
render() {
return html`
<ha-progress-button
.progress="${this.progress}"
@click="${this._buttonTapped}"
?disabled="${this.disabled}"
><slot></slot
></ha-progress-button>
`;
}
constructor() {
super();
this.method = "POST";
this.data = {};
this.disabled = false;
this.progress = false;
}
static get properties() {
return {
hass: {},
progress: Boolean,
path: String,
method: String,
data: {},
disabled: Boolean,
};
}
get progressButton() {
return this.renderRoot.querySelector("ha-progress-button");
}
async _buttonTapped() {
this.progress = true;
const eventData = {
method: this.method,
path: this.path,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path, this.data);
this.progress = false;
this.progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err) {
this.progress = false;
this.progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData);
}
}
customElements.define("ha-call-api-button", HaCallApiButton);

View File

@@ -1,77 +0,0 @@
import { css, CSSResult, html, LitElement, property, query } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "./ha-progress-button";
class HaCallApiButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public method: "POST" | "GET" | "PUT" | "DELETE" = "POST";
@property() public data = {};
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public path?: string;
@query("ha-progress-button", true) private _progressButton;
render() {
return html`
<ha-progress-button
.progress=${this.progress}
@click=${this._buttonTapped}
?disabled=${this.disabled}
><slot></slot
></ha-progress-button>
`;
}
async _buttonTapped() {
this.progress = true;
const eventData: {
method: string;
path: string;
data: any;
success?: boolean;
response?: any;
} = {
method: this.method,
path: this.path!,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path!, this.data);
this.progress = false;
this._progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err) {
this.progress = false;
this._progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData as any);
}
static get styles(): CSSResult {
return css`
:host([disabled]) {
pointer-events: none;
}
`;
}
}
customElements.define("ha-call-api-button", HaCallApiButton);
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;
}
}

View File

@@ -63,7 +63,7 @@ export interface DataTableSortColumnData {
}
export interface DataTableColumnData extends DataTableSortColumnData {
title: TemplateResult | string;
title: string;
type?: "numeric" | "icon" | "icon-button";
template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
@@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
}
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: TemplateResult | string;
title?: string;
};
export interface DataTableRowData {
@@ -132,7 +132,7 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
@internalProperty() private _items: DataTableRowData[] = [];
private _items: DataTableRowData[] = [];
private _checkableRowsCount?: number;
@@ -160,9 +160,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this._items.length) {
if (this._filteredData.length) {
// Force update of location of rows
this._items = [...this._items];
this._filteredData = [...this._filteredData];
}
}
@@ -236,19 +236,20 @@ export class HaDataTable extends LitElement {
"auto-height": this.autoHeight,
})}"
role="table"
aria-rowcount=${this._filteredData.length + 1}
aria-rowcount=${this._filteredData.length}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
<div class="mdc-data-table__header-row" role="row">
${this.selectable
? html`
<div
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@@ -291,13 +292,7 @@ export class HaDataTable extends LitElement {
})
: ""}
role="columnheader"
aria-sort=${ifDefined(
sorted
? this._sortDirection === "desc"
? "descending"
: "ascending"
: undefined
)}
scope="col"
@click=${this._handleHeaderClick}
.columnId=${key}
>
@@ -343,7 +338,7 @@ export class HaDataTable extends LitElement {
}
return html`
<div
aria-rowindex=${index! + 2}
aria-rowindex=${index}
role="row"
.rowId=${row[this.id]}
@click=${this._handleRowClick}
@@ -550,9 +545,7 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() {
// force scroller to update, change it's items
if (this._items.length) {
this._items = [...this._items];
}
this._filteredData = [...this._filteredData];
fireEvent(this, "selection-changed", {
value: this._checkedRows,
});

View File

@@ -1,12 +1,16 @@
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-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -34,8 +38,7 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box";
import "../ha-svg-icon";
interface Device {
name: string;
@@ -100,7 +103,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
public excludeDomains?: string[];
/**
* Show only devices with entities of these device classes.
* Show only deviced with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@@ -109,11 +112,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean })
private _opened?: boolean;
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _init = false;
@@ -242,11 +244,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
);
public open() {
this.comboBox?.open();
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.comboBox?.focus();
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -269,7 +275,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getDevices(
(this._comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
@@ -286,29 +292,70 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
<vaadin-combo-box-light
item-value-path="id"
item-id-path="id"
item-label-path="name"
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
></ha-combo-box>
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
@@ -316,10 +363,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {

View File

@@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors(
return value;
}
const date = new Date(values[index].value);
return formatTime(date, this.hass.locale);
return formatTime(date, this.hass.language);
}
drawChart() {

View File

@@ -99,7 +99,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
public open() {
this.updateComplete.then(() => {
@@ -208,7 +208,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter,
this.includeDeviceClasses
);
(this.comboBox as any).filteredItems = this._states;
(this._comboBox as any).filteredItems = this._states;
this._initedStates = true;
}
}
@@ -296,7 +296,7 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter(
(this._comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)

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