Compare commits

..

2 Commits

Author SHA1 Message Date
Ludeeus b77ce9fc0e localize 2021-03-05 14:40:05 +00:00
Ludeeus bc14d6dfcb Add supervisor-connectivity 2021-03-05 14:33:01 +00:00
280 changed files with 3990 additions and 17478 deletions
+1 -2
View File
@@ -39,7 +39,7 @@ class HcLovelace extends LitElement {
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
locale: this.hass.locale,
language: "en",
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
@@ -94,7 +94,6 @@ class HcLovelace extends LitElement {
return css`
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
-382
View File
@@ -1,382 +0,0 @@
import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_action: "action/2",
last_condition: "condition/0",
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: {
"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,
},
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",
},
},
},
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",
},
],
};
@@ -1,247 +0,0 @@
import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_action: "action/3",
last_condition: null,
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: {
"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,
},
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",
},
},
},
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",
},
],
};
-7
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[];
}
@@ -1,64 +0,0 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
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;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .heading=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace-timeline
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace-timeline>
</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;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace": DemoAutomationTrace;
}
}
+1 -5
View File
@@ -81,8 +81,4 @@ class DemoMoreInfoLight extends LitElement {
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-light": DemoMoreInfoLight;
}
}
customElements.define("demo-more-info-light", DemoMoreInfoLight);
+35 -7
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"));
}
}
@@ -1,3 +1,4 @@
import "../components/supervisor-connectivity";
import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
@@ -133,6 +134,8 @@ class HassioAddonStore extends LitElement {
</div>
`
: ""}
<supervisor-connectivity .supervisor=${this.supervisor}>
</supervisor-connectivity>
</hass-tabs-subpage>
`;
}
@@ -1,3 +1,4 @@
import "../components/supervisor-connectivity";
import {
mdiCogs,
mdiFileDocument,
@@ -129,6 +130,8 @@ class HassioAddonDashboard extends LitElement {
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-router>
<supervisor-connectivity .supervisor=${this.supervisor}>
</supervisor-connectivity>
</hass-tabs-subpage>
`;
}
@@ -177,8 +180,9 @@ class HassioAddonDashboard extends LitElement {
const requestedAddon = extractSearchParam("addon");
if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons
.some((addon) => addon.slug === requestedAddon);
const validAddon = addonsInfo.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
@@ -242,14 +242,14 @@ 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">
${this.supervisor.localize("addon.dashboard.changelog")} </span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}>${
this.supervisor.localize("addon.dashboard.changelog")
}</span>`}
: html`<span class="changelog-link" @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</span>`}
</div>
<div class="description light-color">
@@ -0,0 +1,54 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
@customElement("supervisor-connectivity")
class SupervisorConnectivity extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
protected render(): TemplateResult {
if (this.supervisor.network.supervisor_internet) {
return html``;
}
return html`<div class="connectivity">
<span>${this.supervisor.localize("common.error.lost_connectivity")}</span>
</div>`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.connectivity {
position: fixed;
bottom: 0;
height: 32px;
width: 100vw;
background-color: var(--error-color);
color: var(--primary-text-color);
font-weight: 500;
display: flex;
align-items: center;
}
span {
padding-left: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-connectivity": SupervisorConnectivity;
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ class SupervisorMetric extends LitElement {
</span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value">
${roundedValue} %
${roundedValue}%
</span>
<ha-bar
class="${classMap({
+3
View File
@@ -1,3 +1,4 @@
import "../components/supervisor-connectivity";
import {
css,
CSSResult,
@@ -49,6 +50,8 @@ class HassioDashboard extends LitElement {
.supervisor=${this.supervisor}
></hassio-addons>
</div>
<supervisor-connectivity .supervisor=${this.supervisor}>
</supervisor-connectivity>
</hass-tabs-subpage>
`;
}
@@ -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";
+3
View File
@@ -1,3 +1,4 @@
import "../components/supervisor-connectivity";
import "@material/mwc-button";
import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
@@ -269,6 +270,8 @@ class HassioSnapshots extends LitElement {
)}
</div>
</div>
<supervisor-connectivity .supervisor=${this.supervisor}>
</supervisor-connectivity>
</hass-tabs-subpage>
`;
}
+2 -2
View File
@@ -154,8 +154,8 @@ class HassioHostInfo extends LitElement {
)}
</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>`
: ""}
+24 -29
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";
@@ -49,7 +48,6 @@ const UNSUPPORTED_REASON_URL = {
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
};
const UNHEALTHY_REASON_URL = {
@@ -57,7 +55,6 @@ const UNHEALTHY_REASON_URL = {
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
};
@customElement("hassio-supervisor-info")
@@ -151,32 +148,30 @@ class HassioSupervisorInfo extends LitElement {
</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">
${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`<div class="error">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
+3
View File
@@ -1,3 +1,4 @@
import "../components/supervisor-connectivity";
import {
css,
CSSResult,
@@ -62,6 +63,8 @@ class HassioSystem extends LitElement {
.supervisor=${this.supervisor}
></hassio-supervisor-log>
</div>
<supervisor-connectivity .supervisor=${this.supervisor}>
</supervisor-connectivity>
</hass-tabs-subpage>
`;
}
-1
View File
@@ -132,7 +132,6 @@
"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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+2 -2
View File
@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210331.0",
version="20210302.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,
-2
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",
+4 -5
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",
+4 -5
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",
+6 -7
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",
+7 -9
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,9 +67,8 @@ export const computeStateDisplay = (
}
}
// `counter` and `number` domains do not have a unit of measurement but should still use `formatNumber`
if (domain === "counter" || domain === "number") {
return formatNumber(compareState, locale);
if (domain === "counter") {
return formatNumber(compareState, language);
}
return (
@@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
export interface ScorableTextItem {
score?: number;
filterText: string;
text: string;
altText?: string;
}
@@ -55,8 +55,8 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.filterText, item.altText)
: fuzzySequentialMatch(filter, item.filterText);
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item;
})
.filter((item) => item.score !== undefined && item.score > 0)
+9 -45
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,27 +16,13 @@ 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 ? num.toString() : "";
return num.toString();
};
/**
-17
View File
@@ -1,17 +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) => hasTemplate(val));
}
return false;
};
-2
View File
@@ -1,2 +0,0 @@
export const strStartsWith = (value: string, search: string) =>
value.substring(0, search.length) === search;
-5
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;
};
-14
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();
};
@@ -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);
@@ -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;
}
}
+9 -16
View File
@@ -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,
});
+4 -4
View File
@@ -113,7 +113,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
private _init = false;
@@ -242,11 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
);
public open() {
this.comboBox?.open();
this._comboBox?.open();
}
public focus() {
this.comboBox?.focus();
this._comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -269,7 +269,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!,
+1 -1
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() {
+8 -17
View File
@@ -17,7 +17,6 @@ import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-switch";
import "../ha-formfield";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -30,8 +29,6 @@ export class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity;
@property() public label?: string;
@internalProperty() private _isOn = false;
protected render(): TemplateResult {
@@ -58,21 +55,15 @@ export class HaEntityToggle extends LitElement {
`;
}
const switchTemplate = html`<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>`;
if (!this.label) {
return switchTemplate;
}
return html`
<ha-formfield .label=${this.label}>${switchTemplate}</ha-formfield>
<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>
`;
}
@@ -116,8 +116,12 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN
? "-"
: state.attributes.unit_of_measurement
? formatNumber(state.state, this.hass!.locale)
: computeStateDisplay(this.hass!.localize, state, this.hass!.locale);
? formatNumber(state.state, this.hass!.language)
: computeStateDisplay(
this.hass!.localize,
state,
this.hass!.language
);
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ class StateInfo extends LitElement {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
-188
View File
@@ -1,188 +0,0 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"];
declare global {
interface HASSDomEvents {
"analytics-preferences-changed": { preferences: AnalyticsPreferences };
}
}
@customElement("ha-analytics")
export class HaAnalytics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public analytics!: Analytics;
protected render(): TemplateResult {
if (!this.analytics.huuid) {
return html``;
}
const enabled = this.analytics.preferences.base;
return html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.analytics.instance_id",
"huuid",
this.analytics.huuid
)}
</p>
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${enabled}
.preference=${"base"}
>
</ha-checkbox>
</span>
<span slot="heading">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.base.title`
)}
</span>
<span slot="description">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.base.description`
)}
</span>
</ha-settings-row>
${ADDITIONAL_PREFERENCES.map(
(preference) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics.preferences[preference]}
.preference=${preference}
.disabled=${!enabled}
>
</ha-checkbox>
${!enabled
? html`<paper-tooltip animation-delay="0" position="right"
>${this.hass.localize(
"ui.panel.config.core.section.core.analytics.needs_base"
)}
</paper-tooltip>`
: ""}
</span>
<span slot="heading">
${preference === "usage"
? isComponentLoaded(this.hass, "hassio")
? this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.title`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage.title`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.${preference}.title`
)}
</span>
<span slot="description">
${preference === "usage"
? isComponentLoaded(this.hass, "hassio")
? this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.description`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage.description`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
)}
</span>
</ha-settings-row>`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics.preferences.diagnostics}
.preference=${"diagnostics"}
>
</ha-checkbox>
</span>
<span slot="heading">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
)}
</span>
<span slot="description">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.description`
)}
</span>
</ha-settings-row>
<p>
<a
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.core.section.core.analytics.learn_more"
)}
</a>
</p>
`;
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
const preferences = { ...this.analytics.preferences };
if (checkbox.checked) {
if (preferences[preference]) {
return;
}
preferences[preference] = true;
} else {
preferences[preference] = false;
}
fireEvent(this, "analytics-preferences-changed", { preferences });
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-analytics": HaAnalytics;
}
}
+5 -5
View File
@@ -127,7 +127,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _init = false;
@@ -140,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
this._entities = entities.filter((entity) => entity.area_id);
}),
];
}
@@ -193,13 +193,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
inputEntities = entities;
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
inputEntities = entities;
}
}
@@ -319,7 +319,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getAreas(
(this._comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
+3
View File
@@ -1,3 +1,4 @@
import "@material/mwc-button";
import "@material/mwc-menu";
import type { Corner, Menu } from "@material/mwc-menu";
import {
@@ -10,6 +11,8 @@ import {
query,
TemplateResult,
} from "lit-element";
import "./ha-icon-button";
@customElement("ha-button-menu")
export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START";
@@ -1,163 +0,0 @@
import "@material/mwc-icon-button";
import type { Corner } from "@material/mwc-menu";
import { mdiFilterVariant } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "@material/mwc-menu/mwc-menu-surface";
import { fireEvent } from "../common/dom/fire_event";
import { findRelated, RelatedResult } from "../data/search";
import type { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-area-picker";
import "./device/ha-device-picker";
declare global {
// for fire event
interface HASSDomEvents {
"related-changed": {
value?: FilterValue;
items?: RelatedResult;
filter?: string;
};
}
}
interface FilterValue {
area?: string;
device?: string;
}
@customElement("ha-button-related-filter-menu")
export class HaRelatedFilterButtonMenu extends LitElement {
@property() public hass!: HomeAssistant;
@property() public corner: Corner = "TOP_START";
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public value?: FilterValue;
@internalProperty() private _open = false;
protected render(): TemplateResult {
return html`
<mwc-icon-button @click=${this._handleClick}>
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
</mwc-icon-button>
<mwc-menu-surface
.open=${this._open}
.anchor=${this}
.fullwidth=${this.narrow}
.corner=${this.corner}
@closed=${this._onClosed}
>
<ha-area-picker
.label=${this.hass.localize(
"ui.components.related-filter-menu.filter_by_area"
)}
.hass=${this.hass}
.value=${this.value?.area}
no-add
@value-changed=${this._areaPicked}
></ha-area-picker>
<ha-device-picker
.label=${this.hass.localize(
"ui.components.related-filter-menu.filter_by_device"
)}
.hass=${this.hass}
.value=${this.value?.device}
@value-changed=${this._devicePicked}
></ha-device-picker>
</mwc-menu-surface>
`;
}
private _handleClick(): void {
if (this.disabled) {
return;
}
this._open = true;
}
private _onClosed(): void {
this._open = false;
}
private async _devicePicked(ev: CustomEvent) {
const deviceId = ev.detail.value;
if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined });
return;
}
const filter = this.hass.localize(
"ui.components.related-filter-menu.filtered_by_device",
"device_name",
(ev.currentTarget as any).comboBox.selectedItem.name
);
const items = await findRelated(this.hass, "device", deviceId);
fireEvent(this, "related-changed", {
value: { device: deviceId },
filter,
items,
});
}
private async _areaPicked(ev: CustomEvent) {
const areaId = ev.detail.value;
if (!areaId) {
fireEvent(this, "related-changed", { value: undefined });
return;
}
const filter = this.hass.localize(
"ui.components.related-filter-menu.filtered_by_area",
"area_name",
(ev.currentTarget as any).comboBox.selectedItem.name
);
const items = await findRelated(this.hass, "area", areaId);
fireEvent(this, "related-changed", {
value: { area: areaId },
filter,
items,
});
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
position: relative;
}
:host([narrow]) {
position: static;
}
ha-area-picker,
ha-device-picker {
display: block;
width: 300px;
padding: 4px 16px;
box-sizing: border-box;
}
:host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
}
}
-74
View File
@@ -1,74 +0,0 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import "./ha-icon";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
unsafeCSS,
} from "lit-element";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip")
export class HaChip extends LitElement {
@property() public index = 0;
@property({ type: Boolean }) public hasIcon = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip" .index=${this.index}>
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
<slot name="icon"></slot>
</div>`
: null}
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text"><slot></slot></span>
</span>
</span>
</div>
`;
}
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: var(
--ha-chip-background-color,
rgba(var(--rgb-primary-text-color), 0.15)
);
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip:hover {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip__icon--leading {
--mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-chip": HaChip;
}
}
@@ -1,5 +1,6 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import {
css,
CSSResult,
@@ -11,7 +12,6 @@ import {
unsafeCSS,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
@@ -20,8 +20,8 @@ declare global {
}
}
@customElement("ha-chip-set")
export class HaChipSet extends LitElement {
@customElement("ha-chips")
export class HaChips extends LitElement {
@property() public items = [];
protected render(): TemplateResult {
@@ -33,9 +33,18 @@ export class HaChipSet extends LitElement {
${this.items.map(
(item, idx) =>
html`
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
<span role="gridcell">
<span
role="button"
tabindex="0"
class="mdc-chip__primary-action"
>
<span class="mdc-chip__text">${item}</span>
</span>
</span>
</div>
`
)}
</div>
@@ -51,9 +60,9 @@ export class HaChipSet extends LitElement {
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
ha-chip {
margin: 4px;
.mdc-chip {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
color: var(--primary-text-color);
}
`;
}
@@ -61,6 +70,6 @@ export class HaChipSet extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-chip-set": HaChipSet;
"ha-chips": HaChips;
}
}
+9 -9
View File
@@ -53,14 +53,14 @@ class HaClimateState extends LitElement {
if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
this.hass!.language
)} %`;
}
@@ -78,17 +78,17 @@ class HaClimateState extends LitElement {
) {
return `${formatNumber(
this.stateObj.attributes.target_temp_low,
this.hass.locale
this.hass!.language
)}-${formatNumber(
this.stateObj.attributes.target_temp_high,
this.hass.locale
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
}
if (
@@ -97,17 +97,17 @@ class HaClimateState extends LitElement {
) {
return `${formatNumber(
this.stateObj.attributes.target_humidity_low,
this.hass.locale
this.hass!.language
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass.locale
)} %`;
this.hass!.language
)}%`;
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
this.hass!.language
)} %`;
}
-11
View File
@@ -47,17 +47,6 @@ export class HaCodeEditor extends UpdatingElement {
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public get hasComments(): boolean {
if (!this.codemirror || !this._loadedCodeMirror) {
return false;
}
const className = this._loadedCodeMirror.HighlightStyle.get(
this.codemirror.state,
this._loadedCodeMirror.tags.comment
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
+2 -6
View File
@@ -86,10 +86,6 @@ export class HaComboBox extends LitElement {
});
}
public get selectedItem() {
return (this._comboBox as any).selectedItem;
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
@@ -153,9 +149,9 @@ export class HaComboBox extends LitElement {
fireEvent(this, ev.type, ev.detail);
}
private _filterChanged(ev: PolymerChangedEvent<string>) {
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
// @ts-ignore
fireEvent(this, ev.type, ev.detail, { composed: false });
fireEvent(this, ev.type, ev.detail);
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
+3 -3
View File
@@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
if (!oldHass || oldHass.language !== this.hass.language) {
this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass);
}
@@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.locale)}
.value=${formatDateTime(this.startDate, this.hass.language)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
@@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.locale)}
.value=${formatDateTime(this.endDate, this.hass.language)}
label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
+2 -3
View File
@@ -11,7 +11,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => {
@@ -30,7 +29,7 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0;
@property() public locale!: FrontendTranslationData;
@property({ type: String }) public language = "";
@property() public label = "";
@@ -91,7 +90,7 @@ export class Gauge extends LitElement {
</svg>
<svg class="text">
<text class="value-text">
${formatNumber(this.value, this.locale)} ${this.label}
${formatNumber(this.value, this.language)} ${this.label}
</text>
</svg>`;
}
+25 -35
View File
@@ -36,7 +36,6 @@ interface ExtHassService extends Omit<HassService, "fields"> {
example?: any;
selector?: Selector;
}[];
hasSelector: string[];
}
@customElement("ha-service-control")
@@ -53,6 +52,8 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@@ -69,11 +70,13 @@ export class HaServiceControl extends LitElement {
this._checkedKeys = new Set();
}
const serviceData = this._getServiceInfo(this.value?.service);
this._serviceData = this.value?.service
? this._getServiceInfo(this.value.service)
: undefined;
if (
serviceData &&
"target" in serviceData &&
this._serviceData &&
"target" in this._serviceData &&
(this.value?.data?.entity_id ||
this.value?.data?.area_id ||
this.value?.data?.device_id)
@@ -116,7 +119,7 @@ export class HaServiceControl extends LitElement {
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
});
private _getServiceInfo = memoizeOne((service?: string):
private _getServiceInfo = memoizeOne((service: string):
| ExtHassService
| undefined => {
if (!service) {
@@ -144,29 +147,23 @@ export class HaServiceControl extends LitElement {
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
};
});
protected render() {
const serviceData = this._getServiceInfo(this.value?.service);
const shouldRenderServiceDataYaml =
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
(serviceData &&
Object.keys(this.value?.data || {}).some(
(key) => !serviceData!.hasSelector.includes(key)
));
const legacy =
this._serviceData?.fields.length &&
!this._serviceData.fields.some((field) => field.selector);
const entityId =
shouldRenderServiceDataYaml &&
serviceData?.fields.find((field) => field.key === "entity_id");
legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.fields.some((field) => field.selector && !field.required)
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
);
return html`<ha-service-picker
@@ -174,8 +171,8 @@ export class HaServiceControl extends LitElement {
.value=${this.value?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<p>${serviceData?.description}</p>
${serviceData && "target" in serviceData
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
@@ -191,8 +188,8 @@ export class HaServiceControl extends LitElement {
)}</span
><ha-selector
.hass=${this.hass}
.selector=${serviceData.target
? { target: serviceData.target }
.selector=${this._serviceData.target
? { target: this._serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
@@ -212,7 +209,7 @@ export class HaServiceControl extends LitElement {
allow-custom-entity
></ha-entity-picker>`
: ""}
${shouldRenderServiceDataYaml
${legacy
? html`<ha-yaml-editor
.label=${this.hass.localize(
"ui.components.service-control.service_data"
@@ -221,12 +218,8 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this.value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) =>
dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined))
: this._serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced)
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional
@@ -335,10 +328,7 @@ export class HaServiceControl extends LitElement {
ev.stopPropagation();
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
this.value?.data?.[key] === value ||
(!this.value?.data?.[key] && (value === "" || value === undefined))
) {
if (this.value?.data && this.value.data[key] === value) {
return;
}
+2 -2
View File
@@ -245,7 +245,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize ||
hass.locale !== oldHass.locale ||
hass.language !== oldHass.language ||
hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel
);
@@ -281,7 +281,7 @@ class HaSidebar extends LitElement {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
+3 -14
View File
@@ -2,7 +2,6 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/string/format_number";
import LocalizeMixin from "../mixins/localize-mixin";
/*
@@ -56,31 +55,21 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined.
if (
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${formatNumber(
stateObj.attributes.target_temp_low,
this.hass.locale
)} - ${formatNumber(
stateObj.attributes.target_temp_high,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${formatNumber(
stateObj.attributes.temperature,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
return "";
}
_localizeState(stateObj) {
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
return computeStateDisplay(this.hass.localize, stateObj);
}
}
customElements.define("ha-water_heater-state", HaWaterHeaterState);
-10
View File
@@ -107,10 +107,6 @@ export class PaperTimeInput extends PolymerElement {
#millisec {
width: 38px;
}
.no-suffix {
margin-left: -2px;
}
</style>
<label hidden$="[[hideLabel]]">[[label]]</label>
@@ -138,7 +134,6 @@ export class PaperTimeInput extends PolymerElement {
<!-- Min Input -->
<paper-input
class$="[[_computeClassNames(enableSecond)]]"
id="min"
type="number"
value="{{min}}"
@@ -160,7 +155,6 @@ export class PaperTimeInput extends PolymerElement {
<!-- Sec Input -->
<paper-input
class$="[[_computeClassNames(enableMillisecond)]]"
id="sec"
type="number"
value="{{sec}}"
@@ -485,10 +479,6 @@ export class PaperTimeInput extends PolymerElement {
_equal(n1, n2) {
return n1 === n2;
}
_computeClassNames(hasSuffix) {
return hasSuffix ? " " : "no-suffix";
}
}
customElements.define("paper-time-input", PaperTimeInput);
+1 -1
View File
@@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTimeWithSeconds(date, this.hass.locale);
return formatDateTimeWithSeconds(date, this.hass.language);
};
const chartOptions = {
@@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const formatTooltipLabel = (item, data) => {
const values = data.datasets[item.datasetIndex].data[item.index];
const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
const state = values[2];
return [state, start, end];
-125
View File
@@ -1,125 +0,0 @@
import { mdiCircleOutline } from "@mdi/js";
import {
LitElement,
customElement,
html,
css,
property,
TemplateResult,
internalProperty,
} from "lit-element";
import { buttonLinkStyle } from "../../resources/styles";
import "../ha-svg-icon";
@customElement("ha-timeline")
export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public label = false;
@property({ type: Boolean, reflect: true }) public raised = false;
@property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string;
@property({ attribute: false }) public moreItems?: TemplateResult[];
@internalProperty() private _showMore = false;
protected render() {
return html`
<div class="timeline-start">
${this.label
? ""
: html`
<ha-svg-icon .path=${this.icon || mdiCircleOutline}></ha-svg-icon>
`}
${this.lastItem ? "" : html`<div class="line"></div>`}
</div>
<div class="content">
<slot></slot>
${!this.moreItems
? ""
: html`
<div>
${this._showMore ||
// If there is only 1 item hidden behind "show more", just show it
// instead of showing the more info link. We're not animals.
this.moreItems.length === 1
? this.moreItems
: html`
<button class="link" @click=${this._handleShowMore}>
Show ${this.moreItems.length} more items
</button>
`}
</div>
`}
</div>
`;
}
private _handleShowMore() {
this._showMore = true;
}
static get styles() {
return [
css`
:host {
display: flex;
flex-direction: row;
}
:host(:not([lastItem])) {
min-height: 50px;
}
:host([label]) {
margin-top: -8px;
font-style: italic;
color: var(--timeline-label-color, var(--secondary-text-color));
}
.timeline-start {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 8px;
width: 24px;
}
ha-svg-icon {
color: var(
--timeline-ball-color,
var(--timeline-color, var(--secondary-text-color))
);
border-radius: 50%;
}
:host([raised]) ha-svg-icon {
transform: scale(1.3);
}
.line {
flex: 1;
width: 2px;
background-color: var(
--timeline-line-color,
var(--timeline-color, var(--secondary-text-color))
);
margin: 4px 0;
}
.content {
margin-top: 2px;
}
:host(:not([lastItem])) .content {
padding-bottom: 16px;
}
:host([label]) .content {
margin-top: 0;
padding-top: 6px;
}
`,
buttonLinkStyle,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-timeline": HaTimeline;
}
}
-187
View File
@@ -1,187 +0,0 @@
import { css, customElement, LitElement, property, svg } from "lit-element";
import { NODE_SIZE, SPACING } from "./hat-graph";
@customElement("hat-graph-node")
export class HatGraphNode extends LitElement {
@property() iconPath?: string;
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) graphstart?: boolean;
@property({ reflect: true, type: Boolean }) nofocus?: boolean;
@property({ reflect: true, type: Number }) badge?: number;
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("tabindex") && !this.nofocus)
this.setAttribute("tabindex", "0");
}
updated() {
const svgEl = this.shadowRoot?.querySelector("svg");
if (!svgEl) {
return;
}
const bbox = svgEl.getBBox();
const extra_height = this.graphstart ? 2 : 1;
const extra_width = SPACING;
svgEl.setAttribute("width", `${bbox.width + extra_width}px`);
svgEl.setAttribute("height", `${bbox.height + extra_height}px`);
svgEl.setAttribute(
"viewBox",
`${Math.ceil(bbox.x - extra_width / 2)}
${Math.ceil(bbox.y - extra_height / 2)}
${bbox.width + extra_width}
${bbox.height + extra_height}`
);
}
render() {
return svg`
<svg
>
${
this.graphstart
? ``
: svg`
<path
class="connector"
d="
M 0 ${-SPACING - NODE_SIZE / 2}
L 0 0
"
line-caps="round"
/>
`
}
<g class="node">
<circle
cx="0"
cy="0"
r="${NODE_SIZE / 2}"
/>
${
this.badge
? svg`
<g class="number">
<circle
cx="8"
cy="${-NODE_SIZE / 2}"
r="8"
></circle>
<text
x="8"
y="${-NODE_SIZE / 2}"
text-anchor="middle"
alignment-baseline="middle"
>${this.badge > 9 ? "9+" : this.badge}</text>
</g>
`
: ""
}
<g
style="pointer-events: none"
transform="translate(${-12} ${-12})"
>
${this.iconPath ? svg`<path class="icon" d="${this.iconPath}"/>` : ""}
</g>
</g>
</svg>
`;
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: column;
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
--active-clr: var(--active-color, var(--primary-color));
--track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-color));
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
--default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white);
--default-icon-clr: var(--icon-color, black);
--icon-clr: var(--stroke-clr);
}
:host(.track) {
--stroke-clr: var(--track-clr);
--icon-clr: var(--default-icon-clr);
}
:host(.active) circle {
--stroke-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
}
:host(:focus) {
outline: none;
}
:host(:hover) circle {
--stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr);
}
:host([disabled]) circle {
stroke: var(--disabled-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
:host([nofocus]):host-context(.active),
:host([nofocus]):host-context(:focus) {
--stroke-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
}
circle,
path.connector {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
circle {
fill: var(--background-clr);
stroke: var(--circle-clr, var(--stroke-clr));
}
.number circle {
fill: var(--track-clr);
stroke: none;
stroke-width: 0;
}
.number text {
font-size: smaller;
}
path.icon {
fill: var(--icon-clr);
}
:host(.triggered) svg {
overflow: visible;
}
:host(.triggered) circle {
animation: glow 10s;
}
@keyframes glow {
0% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
10% {
filter: drop-shadow(0px 0px 10px rgba(var(--rgb-trigger-color), 1));
}
100% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph-node": HatGraphNode;
}
}
-225
View File
@@ -1,225 +0,0 @@
import {
css,
customElement,
html,
LitElement,
property,
svg,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
export const BRANCH_HEIGHT = 20;
export const SPACING = 10;
export const NODE_SIZE = 30;
const track_converter = {
fromAttribute: (value) => value.split(",").map((v) => parseInt(v)),
toAttribute: (value) =>
value instanceof Array ? value.join(",") : `${value}`,
};
export interface NodeInfo {
path: string;
config: any;
}
interface BranchConfig {
x: number;
height: number;
start: boolean;
end: boolean;
}
@customElement("hat-graph")
export class HatGraph extends LitElement {
@property({ type: Number }) _num_items = 0;
@property({ reflect: true, type: Boolean }) branching?: boolean;
@property({ reflect: true, converter: track_converter })
track_start?: number[];
@property({ reflect: true, converter: track_converter }) track_end?: number[];
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) selected?: boolean;
@property({ reflect: true, type: Boolean }) short = false;
async updateChildren() {
this._num_items = this.children.length;
}
render() {
const branches: BranchConfig[] = [];
let total_width = 0;
let max_height = 0;
let min_height = Number.POSITIVE_INFINITY;
if (this.branching) {
for (const c of Array.from(this.children)) {
if (c.slot === "head") continue;
const rect = c.getBoundingClientRect();
branches.push({
x: rect.width / 2 + total_width,
height: rect.height,
start: c.getAttribute("graphStart") != null,
end: c.getAttribute("graphEnd") != null,
});
total_width += rect.width;
max_height = Math.max(max_height, rect.height);
min_height = Math.min(min_height, rect.height);
}
}
return html`
<slot name="head" @slotchange=${this.updateChildren}> </slot>
${this.branching
? svg`
<svg
id="top"
width="${total_width}"
height="${BRANCH_HEIGHT}"
>
${branches.map((branch, i) => {
if (branch.start) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_start?.includes(i) ?? false,
})}"
id="${this.track_start?.includes(i) ? "track-start" : ""}"
index=${i}
d="
M ${total_width / 2} 0
L ${branch.x} ${BRANCH_HEIGHT}
"/>
`;
})}
<use xlink:href="#track-start" />
</svg>
`
: ""}
<div id="branches">
${this.branching
? svg`
<svg
id="lines"
width="${total_width}"
height="${max_height}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
index=${i}
d="
M ${branch.x} ${branch.height}
l 0 ${max_height - branch.height}
"/>
`;
})}
</svg>
`
: ""}
<slot @slotchange=${this.updateChildren}></slot>
</div>
${this.branching && !this.short
? svg`
<svg
id="bottom"
width="${total_width}"
height="${BRANCH_HEIGHT + SPACING}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
id="${this.track_end?.includes(i) ? "track-end" : ""}"
index=${i}
d="
M ${branch.x} 0
L ${branch.x} ${SPACING}
L ${total_width / 2} ${BRANCH_HEIGHT + SPACING}
"/>
`;
})}
<use xlink:href="#track-end" />
</svg>
`
: ""}
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
--active-clr: var(--active-color, var(--primary-color));
--track-clr: var(--track-color, var(--accent-color));
--disabled-clr: var(--disabled-color, gray);
}
:host(:focus) {
outline: none;
}
#branches {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
:host([branching]) #branches {
flex-direction: row;
align-items: start;
}
:host([branching]) ::slotted(*) {
z-index: 1;
}
:host([branching]) ::slotted([slot="head"]) {
margin-bottom: ${-BRANCH_HEIGHT / 2}px;
}
#lines {
position: absolute;
}
path.line {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
path.line.track {
stroke: var(--track-clr);
}
:host([disabled]) path.line {
stroke: var(--disabled-clr);
}
:host(.active) #top path.line {
stroke: var(--active-clr);
}
:host(:focus) #top path.line {
stroke: var(--active-clr);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph": HatGraph;
}
}
-553
View File
@@ -1,553 +0,0 @@
import {
html,
LitElement,
property,
customElement,
PropertyValues,
css,
} from "lit-element";
import "@material/mwc-icon-button/mwc-icon-button";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-svg-icon";
import {
AutomationTraceExtended,
ChooseActionTraceStep,
ConditionTraceStep,
} from "../../data/trace";
import {
mdiAbTesting,
mdiArrowUp,
mdiAsterisk,
mdiCallSplit,
mdiCheckboxBlankOutline,
mdiCheckBoxOutline,
mdiChevronDown,
mdiChevronRight,
mdiChevronUp,
mdiClose,
mdiCodeBrackets,
mdiDevices,
mdiExclamation,
mdiRefresh,
mdiTimerOutline,
mdiTrafficLight,
} from "@mdi/js";
import "./hat-graph-node";
import { classMap } from "lit-html/directives/class-map";
import { NODE_SIZE, SPACING, NodeInfo } from "./hat-graph";
import { Condition, Trigger } from "../../data/automation";
import {
Action,
ChooseAction,
DelayAction,
DeviceAction,
EventAction,
RepeatAction,
SceneAction,
ServiceAction,
WaitAction,
WaitForTriggerAction,
} from "../../data/script";
declare global {
interface HASSDomEvents {
"graph-node-selected": NodeInfo;
}
}
@customElement("hat-script-graph")
class HatScriptGraph extends LitElement {
@property({ attribute: false }) public trace!: AutomationTraceExtended;
@property({ attribute: false }) public selected;
@property() trackedNodes: Record<string, any> = {};
private selectNode(config, path) {
return () => {
fireEvent(this, "graph-node-selected", { config, path });
};
}
private render_trigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const tracked = this.trace && path in this.trace.trace;
if (tracked) {
this.trackedNodes[path] = { config, path };
}
return html`
<hat-graph-node
graphStart
@focus=${this.selectNode(config, path)}
class=${classMap({
track: tracked,
active: this.selected === path,
})}
.iconPath=${mdiAsterisk}
tabindex=${tracked ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_condition(config: Condition, i: number) {
const path = `condition/${i}`;
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
const track_path =
trace === undefined ? 0 : trace![0].result.result ? 1 : 2;
if (trace) {
this.trackedNodes[path] = { config, path };
}
return html`
<hat-graph
branching
@focus=${this.selectNode(config, path)}
class=${classMap({
track: track_path,
active: this.selected === path,
})}
.track_start=${[track_path]}
.track_end=${[track_path]}
tabindex=${trace === undefined ? "-1" : "0"}
short
>
<hat-graph-node
slot="head"
class=${classMap({
track: trace !== undefined,
})}
.iconPath=${mdiAbTesting}
nofocus
graphEnd
></hat-graph-node>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div></div>
<hat-graph-node
.iconPath=${mdiClose}
graphEnd
nofocus
class=${classMap({
track: track_path === 2,
})}
></hat-graph-node>
</hat-graph>
`;
}
private render_choose_node(config: ChooseAction, path: string) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace
? trace[0].result.choice === "default"
? [config.choose.length]
: [trace[0].result.choice]
: [];
return html`
<hat-graph
tabindex=${trace === undefined ? "-1" : "0"}
branching
.track_start=${trace_path}
.track_end=${trace_path}
@focus=${this.selectNode(config, path)}
class=${classMap({
track: trace !== undefined,
active: this.selected === path,
})}
>
<hat-graph-node
.iconPath=${mdiCallSplit}
class=${classMap({
track: trace !== undefined,
})}
slot="head"
nofocus
></hat-graph-node>
${config.choose.map((branch, i) => {
const branch_path = `${path}/choose/${i}`;
return html`
<hat-graph>
<hat-graph-node
.iconPath=${mdiCheckBoxOutline}
nofocus
class=${classMap({
track: trace !== undefined && trace[0].result.choice === i,
})}
></hat-graph-node>
${branch.sequence.map((action, j) =>
this.render_node(action, `${branch_path}/sequence/${j}`)
)}
</hat-graph>
`;
})}
<hat-graph>
<hat-graph-node
.iconPath=${mdiCheckboxBlankOutline}
nofocus
class=${classMap({
track:
trace !== undefined && trace[0].result.choice === "default",
})}
></hat-graph-node>
${config.default?.map((action, i) =>
this.render_node(action, `${path}/default/${i}`)
)}
</hat-graph>
</hat-graph>
`;
}
private render_condition_node(node: Condition, path: string) {
const trace: any = this.trace.trace[path];
const track_path = trace === undefined ? 0 : trace[0].result.result ? 1 : 2;
return html`
<hat-graph
branching
@focus=${this.selectNode(node, path)}
class=${classMap({
track: track_path,
active: this.selected === path,
})}
.track_start=${[track_path]}
.track_end=${[track_path]}
tabindex=${trace === undefined ? "-1" : "0"}
short
>
<hat-graph-node
slot="head"
class=${classMap({
track: trace,
})}
.iconPath=${mdiAbTesting}
nofocus
graphEnd
></hat-graph-node>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div></div>
<hat-graph-node
.iconPath=${mdiClose}
graphEnd
nofocus
class=${classMap({
track: track_path === 2,
})}
></hat-graph-node>
</hat-graph>
`;
}
private render_delay_node(node: DelayAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiTimerOutline}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_device_node(node: DeviceAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiDevices}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_event_node(node: EventAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_repeat_node(node: RepeatAction, path: string) {
const trace: any = this.trace.trace[path];
const track_path = trace ? [0, 1] : [];
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
return html`
<hat-graph
.track_start=${track_path}
.track_end=${track_path}
tabindex=${trace === undefined ? "-1" : "0"}
branching
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
>
<hat-graph-node
.iconPath=${mdiRefresh}
class=${classMap({
track: trace,
})}
slot="head"
nofocus
></hat-graph-node>
<hat-graph-node
.iconPath=${mdiArrowUp}
nofocus
class=${classMap({
track: track_path.includes(1),
})}
.badge=${repeats}
></hat-graph-node>
<hat-graph>
${node.repeat.sequence.map((action, i) =>
this.render_node(action, `${path}/repeat/sequence/${i}`)
)}
</hat-graph>
</hat-graph>
`;
}
private render_scene_node(node: SceneAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_service_node(node: ServiceAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiChevronRight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_wait_node(
node: WaitAction | WaitForTriggerAction,
path: string
) {
return html`
<hat-graph-node
.iconPath=${mdiTrafficLight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_other_node(node: Action, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiCodeBrackets}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
></hat-graph-node>
`;
}
private render_node(node: Action, path: string) {
const NODE_TYPES = {
choose: this.render_choose_node,
condition: this.render_condition_node,
delay: this.render_delay_node,
device_id: this.render_device_node,
event: this.render_event_node,
repeat: this.render_repeat_node,
scene: this.render_scene_node,
service: this.render_service_node,
wait_template: this.render_wait_node,
wait_for_trigger: this.render_wait_node,
other: this.render_other_node,
};
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
const nodeEl = NODE_TYPES[type].bind(this)(node, path);
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = { config: node, path };
}
return nodeEl;
}
protected render() {
const paths = Object.keys(this.trackedNodes);
const manual_triggered = this.trace && "trigger" in this.trace.trace;
let track_path = manual_triggered ? undefined : [0];
const trigger_nodes = (Array.isArray(this.trace.config.trigger)
? this.trace.config.trigger
: [this.trace.config.trigger]
).map((trigger, i) => {
if (this.trace && `trigger/${i}` in this.trace.trace) {
track_path = [i];
}
return this.render_trigger(trigger, i);
});
return html`
<hat-graph class="parent">
<div></div>
<hat-graph
branching
id="trigger"
.short=${trigger_nodes.length < 2}
.track_start=${track_path}
.track_end=${track_path}
>
${trigger_nodes}
</hat-graph>
<hat-graph id="condition">
${(!this.trace.config.condition ||
Array.isArray(this.trace.config.condition)
? this.trace.config.condition
: [this.trace.config.condition]
)?.map((condition, i) => this.render_condition(condition, i))}
</hat-graph>
${(Array.isArray(this.trace.config.action)
? this.trace.config.action
: [this.trace.config.action]
).map((action, i) => this.render_node(action, `action/${i}`))}
</hat-graph>
<div class="actions">
<mwc-icon-button
.disabled=${paths.length === 0 || paths[0] === this.selected}
@click=${this.previousTrackedNode}
>
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
.disabled=${paths.length === 0 ||
paths[paths.length - 1] === this.selected}
@click=${this.nextTrackedNode}
>
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
</mwc-icon-button>
</div>
`;
}
protected update(changedProps: PropertyValues<this>) {
if (changedProps.has("trace")) {
this.trackedNodes = {};
}
super.update(changedProps);
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
// Select first node if new trace loaded but no selection given.
if (changedProps.has("trace")) {
const tracked = this.getTrackedNodes();
const paths = Object.keys(tracked);
// If trace changed and we have no or an invalid selection, select first option.
if (this.selected === "" || !(this.selected in paths)) {
// Find first tracked node with node info
for (const path of paths) {
if (tracked[path]) {
fireEvent(this, "graph-node-selected", tracked[path]);
break;
}
}
}
if (this.trace) {
const sortKeys = Object.keys(this.trace.trace);
const keys = Object.keys(this.trackedNodes).sort(
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
);
const sortedTrackedNodes = keys.reduce((obj, key) => {
obj[key] = this.trackedNodes[key];
return obj;
}, {});
this.trackedNodes = sortedTrackedNodes;
}
}
}
public getTrackedNodes() {
return this.trackedNodes;
}
public previousTrackedNode() {
const tracked = this.getTrackedNodes();
const nodes = Object.keys(tracked);
for (let i = nodes.indexOf(this.selected) - 1; i >= 0; i--) {
if (tracked[nodes[i]]) {
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
break;
}
}
}
public nextTrackedNode() {
const tracked = this.getTrackedNodes();
const nodes = Object.keys(tracked);
for (let i = nodes.indexOf(this.selected) + 1; i < nodes.length; i++) {
if (tracked[nodes[i]]) {
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
break;
}
}
}
static get styles() {
return css`
:host {
display: flex;
}
.actions {
display: flex;
flex-direction: column;
}
.parent {
margin-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-script-graph": HatScriptGraph;
}
}
-511
View File
@@ -1,511 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
AutomationTraceExtended,
ChooseActionTraceStep,
getDataFromPath,
TriggerTraceStep,
isTriggerPath,
} from "../../data/trace";
import { HomeAssistant } from "../../types";
import "./ha-timeline";
import type { HaTimeline } from "./ha-timeline";
import {
mdiCircle,
mdiCircleOutline,
mdiPauseCircleOutline,
mdiRecordCircleOutline,
} from "@mdi/js";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
ChooseActionChoice,
getActionType,
} from "../../data/script";
import relativeTime from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
/* eslint max-classes-per-file: "off" */
// Report time entry when more than this time has passed
const SIGNIFICANT_TIME_CHANGE = 1000; // 1 seconds
const isSignificantTimeChange = (a: Date, b: Date) =>
Math.abs(b.getTime() - a.getTime()) > SIGNIFICANT_TIME_CHANGE;
class RenderedTimeTracker {
private lastReportedTime: Date;
constructor(
private hass: HomeAssistant,
private entries: TemplateResult[],
trace: AutomationTraceExtended
) {
this.lastReportedTime = new Date(trace.timestamp.start);
}
setLastReportedTime(date: Date) {
this.lastReportedTime = date;
}
renderTime(from: Date, to: Date): void {
this.entries.push(html`
<ha-timeline label>
${relativeTime(from, this.hass.localize, {
compareTime: to,
includeTense: false,
})}
later
</ha-timeline>
`);
this.lastReportedTime = to;
}
maybeRenderTime(timestamp: Date): boolean {
if (!isSignificantTimeChange(timestamp, this.lastReportedTime)) {
this.lastReportedTime = timestamp;
return false;
}
this.renderTime(this.lastReportedTime, timestamp);
return true;
}
}
class LogbookRenderer {
private curIndex: number;
private pendingItems: Array<[Date, LogbookEntry]> = [];
constructor(
private entries: TemplateResult[],
private timeTracker: RenderedTimeTracker,
private logbookEntries: LogbookEntry[]
) {
// Skip the "automation got triggered item"
this.curIndex =
logbookEntries.length > 0 && logbookEntries[0].domain === "automation"
? 1
: 0;
}
get curItem() {
return this.logbookEntries[this.curIndex];
}
get hasNext() {
return this.curIndex !== this.logbookEntries.length;
}
maybeRenderItem() {
const logbookEntry = this.curItem;
this.curIndex++;
const entryDate = new Date(logbookEntry.when);
if (this.pendingItems.length === 0) {
this.pendingItems.push([entryDate, logbookEntry]);
return;
}
const previousEntryDate = this.pendingItems[
this.pendingItems.length - 1
][0];
// If logbook entry is too long after the last one,
// add a time passed label
if (isSignificantTimeChange(previousEntryDate, entryDate)) {
this._renderLogbookEntries();
this.timeTracker.renderTime(previousEntryDate, entryDate);
}
this.pendingItems.push([entryDate, logbookEntry]);
}
flush() {
if (this.pendingItems.length > 0) {
this._renderLogbookEntries();
}
}
private _renderLogbookEntries() {
this.timeTracker.maybeRenderTime(this.pendingItems[0][0]);
const parts: TemplateResult[] = [];
let i;
for (
i = 0;
i < Math.min(this.pendingItems.length, LOGBOOK_ENTRIES_BEFORE_FOLD);
i++
) {
parts.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
let moreItems: TemplateResult[] | undefined;
// If we didn't render all items, push rest into `moreItems`
if (i < this.pendingItems.length) {
moreItems = [];
for (; i < this.pendingItems.length; i++) {
moreItems.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
}
this.entries.push(html`
<ha-timeline .icon=${mdiCircleOutline} .moreItems=${moreItems}>
${parts}
</ha-timeline>
`);
// Clear rendered items.
this.timeTracker.setLastReportedTime(
this.pendingItems[this.pendingItems.length - 1][0]
);
this.pendingItems = [];
}
private _renderLogbookEntryHelper(entry: LogbookEntry) {
return html`${entry.name} (${entry.entity_id})
${entry.message || `turned ${entry.state}`}<br />`;
}
}
class ActionRenderer {
private curIndex = 0;
private keys: string[];
constructor(
private hass: HomeAssistant,
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
private timeTracker: RenderedTimeTracker
) {
this.keys = Object.keys(trace.trace);
}
get curItem() {
return this._getItem(this.curIndex);
}
get hasNext() {
return this.curIndex !== this.keys.length;
}
renderItem() {
this.curIndex = this._renderItem(this.curIndex);
}
private _getItem(index: number) {
return this.trace.trace[this.keys[index]];
}
private _renderItem(
index: number,
actionType?: ReturnType<typeof getActionType>
): number {
const value = this._getItem(index);
if (isTriggerPath(value[0].path)) {
return this._handleTrigger(index, value[0] as TriggerTraceStep);
}
const timestamp = new Date(value[0].timestamp);
// Render all logbook items that are in front of this item.
while (
this.logbookRenderer.hasNext &&
new Date(this.logbookRenderer.curItem.when) < timestamp
) {
this.logbookRenderer.maybeRenderItem();
}
this.logbookRenderer.flush();
this.timeTracker.maybeRenderTime(timestamp);
const path = value[0].path;
let data;
try {
data = getDataFromPath(this.trace.config, path);
} catch (err) {
this.entries.push(
html`Unable to extract path ${path}. Download trace and report as bug`
);
return index + 1;
}
const isTopLevel = path.split("/").length === 2;
if (!isTopLevel && !actionType) {
this._renderEntry(path, path.replace(/\//g, " "));
return index + 1;
}
if (!actionType) {
actionType = getActionType(data);
}
if (actionType === "choose") {
return this._handleChoose(index);
}
this._renderEntry(path, data.alias || actionType);
return index + 1;
}
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
this._renderEntry(
triggerStep.path,
`Triggered ${
triggerStep.path === "trigger"
? "manually"
: `by the ${triggerStep.changed_variables.trigger.description}`
} at
${formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale
)}`,
mdiCircle
);
return index + 1;
}
private _handleChoose(index: number): number {
// startLevel: choose root config
// +1: 'default
// +2: executed sequence
// +1: 'choose'
// +2: current choice
// +3: 'conditions'
// +4: evaluated condition
// +3: 'sequence'
// +4: executed sequence
const choosePath = this.keys[index];
const startLevel = choosePath.split("/").length - 1;
const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep;
const defaultExecuted = chooseTrace.result.choice === "default";
const chooseConfig = this._getDataFromPath(
this.keys[index]
) as ChooseAction;
const name = chooseConfig.alias || "Choose";
if (defaultExecuted) {
this._renderEntry(choosePath, `${name}: Default action executed`);
} else {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice;
const choiceName =
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`;
this._renderEntry(choosePath, `${name}: ${choiceName} executed`);
}
let i;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
const parts = this.keys[i].split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
// We're going to skip all conditions
if (parts[startLevel + 3] === "sequence") {
break;
}
}
// Render choice
while (i < this.keys.length) {
const path = this.keys[i];
const parts = path.split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
// We know it's an action sequence, so force the type like that
// for rendering.
i = this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _renderEntry(
path: string,
description: string,
icon = mdiRecordCircleOutline
) {
this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path}>
${description}
</ha-timeline>
`);
}
private _getDataFromPath(path: string) {
return getDataFromPath(this.trace.config, path);
}
}
@customElement("hat-trace-timeline")
export class HaAutomationTracer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace?: AutomationTraceExtended;
@property({ attribute: false }) public logbookEntries?: LogbookEntry[];
@property({ attribute: false }) public selectedPath?: string;
@property({ type: Boolean }) public allowPick = false;
protected render(): TemplateResult {
if (!this.trace) {
return html``;
}
const entries: TemplateResult[] = [];
const timeTracker = new RenderedTimeTracker(this.hass, entries, this.trace);
const logbookRenderer = new LogbookRenderer(
entries,
timeTracker,
this.logbookEntries || []
);
const actionRenderer = new ActionRenderer(
this.hass,
entries,
this.trace,
logbookRenderer,
timeTracker
);
while (actionRenderer.hasNext) {
actionRenderer.renderItem();
}
while (logbookRenderer.hasNext) {
logbookRenderer.maybeRenderItem();
}
logbookRenderer.flush();
// null means it was stopped by a condition
if (this.trace.last_action !== null) {
entries.push(html`
<ha-timeline
lastItem
.icon=${this.trace.timestamp.finish
? mdiCircle
: mdiPauseCircleOutline}
>
${this.trace.timestamp.finish
? html`Finished at
${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.finish),
this.hass.locale
)}
(runtime:
${(
(new Date(this.trace.timestamp.finish!).getTime() -
new Date(this.trace.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`
: "Still running"}
</ha-timeline>
`);
}
return html`${entries}`;
}
protected updated(props: PropertyValues) {
super.updated(props);
// Pick first path when we load a new trace.
if (
this.allowPick &&
props.has("trace") &&
this.trace &&
this.selectedPath &&
!(this.selectedPath in this.trace.trace)
) {
const element = this.shadowRoot!.querySelector<HaTimeline>(
"ha-timeline[data-path]"
);
if (element) {
fireEvent(this, "value-changed", { value: element.dataset.path });
this.selectedPath = element.dataset.path;
}
}
if (props.has("trace") || props.has("selectedPath")) {
this.shadowRoot!.querySelectorAll<HaTimeline>(
"ha-timeline[data-path]"
).forEach((el) => {
el.style.setProperty(
"--timeline-ball-color",
this.selectedPath === el.dataset.path ? "var(--primary-color)" : null
);
if (!this.allowPick || el.dataset.upgraded) {
return;
}
el.dataset.upgraded = "1";
el.addEventListener("click", () => {
this.selectedPath = el.dataset.path;
fireEvent(this, "value-changed", { value: el.dataset.path });
});
el.addEventListener("mouseover", () => {
el.raised = true;
});
el.addEventListener("mouseout", () => {
el.raised = false;
});
});
}
}
static get styles(): CSSResult[] {
return [
css`
ha-timeline[lastItem].condition {
--timeline-ball-color: var(--error-color);
}
ha-timeline[data-path] {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-trace-timeline": HaAutomationTracer;
}
}
-27
View File
@@ -1,27 +0,0 @@
import { HomeAssistant } from "../types";
export interface AnalyticsPreferences {
base?: boolean;
diagnostics?: boolean;
usage?: boolean;
statistics?: boolean;
}
export interface Analytics {
preferences: AnalyticsPreferences;
huuid: string;
}
export const getAnalyticsDetails = (hass: HomeAssistant) =>
hass.callWS<Analytics>({
type: "analytics",
});
export const setAnalyticsPreferences = (
hass: HomeAssistant,
preferences: AnalyticsPreferences
) =>
hass.callWS<AnalyticsPreferences>({
type: "analytics/preferences",
preferences,
});
+1 -19
View File
@@ -28,17 +28,6 @@ export interface ManualAutomationConfig {
action: Action[];
mode?: typeof MODES[number];
max?: number;
max_exceeded?:
| "silent"
| "critical"
| "fatal"
| "error"
| "warning"
| "warn"
| "info"
| "debug"
| "notset";
variables?: Record<string, unknown>;
}
export interface BlueprintAutomationConfig extends ManualAutomationConfig {
@@ -56,7 +45,7 @@ export interface StateTrigger {
entity_id: string;
attribute?: string;
from?: string | number;
to?: string | string[] | number;
to?: string | number;
for?: string | number | ForDict;
}
@@ -160,13 +149,11 @@ export type Trigger =
export interface LogicalCondition {
condition: "and" | "not" | "or";
alias?: string;
conditions: Condition[];
}
export interface StateCondition {
condition: "state";
alias?: string;
entity_id: string;
attribute?: string;
state: string | number;
@@ -175,7 +162,6 @@ export interface StateCondition {
export interface NumericStateCondition {
condition: "numeric_state";
alias?: string;
entity_id: string;
attribute?: string;
above?: number;
@@ -185,7 +171,6 @@ export interface NumericStateCondition {
export interface SunCondition {
condition: "sun";
alias?: string;
after_offset: number;
before_offset: number;
after: "sunrise" | "sunset";
@@ -194,14 +179,12 @@ export interface SunCondition {
export interface ZoneCondition {
condition: "zone";
alias?: string;
entity_id: string;
zone: string;
}
export interface TimeCondition {
condition: "time";
alias?: string;
after?: string;
before?: string;
weekday?: string | string[];
@@ -209,7 +192,6 @@ export interface TimeCondition {
export interface TemplateCondition {
condition: "template";
alias?: string;
value_template: string;
}
+7 -2
View File
@@ -54,7 +54,7 @@ export const getRecent = (
}
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
(stateHistory) => computeHistory(hass, stateHistory, localize),
(stateHistory) => computeHistory(hass, stateHistory, localize, language),
(err) => {
delete RECENT_CACHE[entityId];
throw err;
@@ -140,7 +140,12 @@ export const getRecentWithCache = (
delete stateHistoryCache[cacheKey];
throw err;
}
const stateHistory = computeHistory(hass, fetchedHistory, localize);
const stateHistory = computeHistory(
hass,
fetchedHistory,
localize,
language
);
if (appendingToCache) {
mergeLine(stateHistory.line, cache.data.line);
mergeTimeline(stateHistory.timeline, cache.data.timeline);
-1
View File
@@ -46,7 +46,6 @@ export interface CloudPreferences {
export type CloudStatusLoggedIn = CloudStatusBase & {
email: string;
google_registered: boolean;
google_entities: EntityFilter;
google_domains: string[];
alexa_entities: EntityFilter;
+2 -2
View File
@@ -1,5 +1,4 @@
import { HaFormSchema } from "../components/ha-form/ha-form";
import { ConfigEntry } from "./config_entries";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";
@@ -45,7 +44,8 @@ export interface DataEntryFlowStepCreateEntry {
flow_id: string;
handler: string;
title: string;
result: ConfigEntry;
// Config entry ID
result: string;
description: string;
description_placeholders: Record<string, string>;
}
+2 -5
View File
@@ -3,18 +3,15 @@ import { HaFormSchema } from "../components/ha-form/ha-form";
import { HomeAssistant } from "../types";
export interface DeviceAutomation {
alias?: string;
device_id: string;
domain: string;
entity_id?: string;
entity_id: string;
type?: string;
subtype?: string;
event?: string;
}
export interface DeviceAction extends DeviceAutomation {
entity_id: string;
}
export type DeviceAction = DeviceAutomation;
export interface DeviceCondition extends DeviceAutomation {
condition: string;
+1 -1
View File
@@ -104,7 +104,7 @@ export const configSyncOS = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/os/config/sync",
endpoint: "os/config/sync",
method: "post",
timeout: null,
});
+2
View File
@@ -50,6 +50,8 @@ export interface WifiConfiguration {
export interface NetworkInfo {
interfaces: NetworkInterface[];
docker: DockerNetwork;
supervisor_internet: boolean;
host_internet: boolean | null;
}
export const fetchNetworkInfo = async (
+4 -4
View File
@@ -4,7 +4,6 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { FrontendTranslationData } from "./translation";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const LINE_ATTRIBUTES_TO_KEEP = [
@@ -110,7 +109,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = (
localize: LocalizeFunc,
language: FrontendTranslationData,
language: string,
states: HassEntity[]
): TimelineEntity => {
const data: TimelineState[] = [];
@@ -204,7 +203,8 @@ const processLineChartEntities = (
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HassEntity[][],
localize: LocalizeFunc
localize: LocalizeFunc,
language: string
): HistoryResult => {
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
const timelineDevices: TimelineEntity[] = [];
@@ -235,7 +235,7 @@ export const computeHistory = (
if (!unit) {
timelineDevices.push(
processTimelineEntity(localize, hass.locale, stateInfo)
processTimelineEntity(localize, language, stateInfo)
);
} else if (unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo);
+2 -5
View File
@@ -25,11 +25,8 @@ export const integrationIssuesUrl = (
manifest.issue_tracker ||
`https://github.com/home-assistant/home-assistant/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+${domain}%22`;
export const domainToName = (
localize: LocalizeFunc,
domain: string,
manifest?: IntegrationManifest
) => localize(`component.${domain}.title`) || manifest?.name || domain;
export const domainToName = (localize: LocalizeFunc, domain: string) =>
localize(`component.${domain}.title`) || domain;
export const fetchIntegrationManifests = (hass: HomeAssistant) =>
hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
+15 -55
View File
@@ -14,9 +14,7 @@ export interface LogbookEntry {
message?: string;
entity_id?: string;
icon?: string;
source?: string;
domain?: string;
context_id?: string;
domain: string;
context_user_id?: string;
context_event_type?: string;
context_domain?: string;
@@ -31,45 +29,21 @@ const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {};
export const getLogbookDataForContext = async (
hass: HomeAssistant,
startDate: string,
contextId?: string
): Promise<LogbookEntry[]> =>
addLogbookMessage(
hass,
await getLogbookDataFromServer(
hass,
startDate,
undefined,
undefined,
undefined,
contextId
)
);
export const getLogbookData = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string,
entity_matches_only?: boolean
): Promise<LogbookEntry[]> =>
addLogbookMessage(
) => {
const logbookData = await getLogbookDataCache(
hass,
await getLogbookDataCache(
hass,
startDate,
endDate,
entityId,
entity_matches_only
)
startDate,
endDate,
entityId,
entity_matches_only
);
export const addLogbookMessage = (
hass: HomeAssistant,
logbookData: LogbookEntry[]
): LogbookEntry[] => {
for (const entry of logbookData) {
const stateObj = hass!.states[entry.entity_id!];
if (entry.state && stateObj) {
@@ -81,6 +55,7 @@ export const addLogbookMessage = (
);
}
}
return logbookData;
};
@@ -125,30 +100,15 @@ export const getLogbookDataCache = async (
const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate?: string,
endDate: string,
entityId?: string,
entitymatchesOnly?: boolean,
contextId?: string
entity_matches_only?: boolean
) => {
const params = new URLSearchParams();
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}${entity_matches_only ? `&entity_matches_only` : ""}`;
if (endDate) {
params.append("end_time", endDate);
}
if (entityId) {
params.append("entity", entityId);
}
if (entitymatchesOnly) {
params.append("entity_matches_only", "");
}
if (contextId) {
params.append("context_id", contextId);
}
return hass.callApi<LogbookEntry[]>(
"GET",
`logbook/${startDate}?${params.toString()}`
);
return hass.callApi<LogbookEntry[]>("GET", url);
};
export const clearLogbookCache = (startDate: string, endDate: string) => {
@@ -323,7 +283,7 @@ export const getLogbookMessage = (
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state",
stateObj
? computeStateDisplay(hass.localize, stateObj, hass.locale, state)
? computeStateDisplay(hass.localize, stateObj, hass.language, state)
: state
);
};
+1 -1
View File
@@ -109,7 +109,7 @@ export interface LovelaceBadgeConfig {
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
view_layout?: any;
layout?: any;
type: string;
[key: string]: any;
}
-7
View File
@@ -12,14 +12,10 @@ export interface OnboardingIntegrationStepResponse {
auth_code: string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface OnboardingAnalyticsStepResponse {}
export interface OnboardingResponses {
user: OnboardingUserStepResponse;
core_config: OnboardingCoreConfigStepResponse;
integration: OnboardingIntegrationStepResponse;
analytics: OnboardingAnalyticsStepResponse;
}
export type ValidOnboardingStep = keyof OnboardingResponses;
@@ -53,9 +49,6 @@ export const onboardCoreConfigStep = (hass: HomeAssistant) =>
"onboarding/core_config"
);
export const onboardAnalyticsStep = (hass: HomeAssistant) =>
hass.callApi<OnboardingAnalyticsStepResponse>("POST", "onboarding/analytics");
export const onboardIntegrationStep = (
hass: HomeAssistant,
params: { client_id: string; redirect_uri: string }
+2 -56
View File
@@ -29,14 +29,12 @@ export interface ScriptConfig {
}
export interface EventAction {
alias?: string;
event: string;
event_data?: Record<string, any>;
event_data_template?: Record<string, any>;
}
export interface ServiceAction {
alias?: string;
service: string;
entity_id?: string;
target?: HassServiceTarget;
@@ -44,7 +42,6 @@ export interface ServiceAction {
}
export interface DeviceAction {
alias?: string;
device_id: string;
domain: string;
entity_id: string;
@@ -58,36 +55,30 @@ export interface DelayActionParts {
days?: number;
}
export interface DelayAction {
alias?: string;
delay: number | Partial<DelayActionParts> | string;
delay: number | Partial<DelayActionParts>;
}
export interface SceneAction {
alias?: string;
scene: string;
}
export interface WaitAction {
alias?: string;
wait_template: string;
timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
alias?: string;
wait_for_trigger: Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
export interface RepeatAction {
alias?: string;
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
interface BaseRepeat {
alias?: string;
sequence: Action[];
}
@@ -103,15 +94,8 @@ export interface UntilRepeat extends BaseRepeat {
until: Condition[];
}
export interface ChooseActionChoice {
alias?: string;
conditions: string | Condition[];
sequence: Action[];
}
export interface ChooseAction {
alias?: string;
choose: ChooseActionChoice[];
choose: [{ conditions: Condition[]; sequence: Action[] }];
default?: Action[];
}
@@ -165,41 +149,3 @@ export const getScriptEditorInitData = () => {
inititialScriptEditorData = undefined;
return data;
};
export const getActionType = (action: Action) => {
// Check based on config_validation.py#determine_script_action
if ("delay" in action) {
return "delay";
}
if ("wait_template" in action) {
return "wait_template";
}
if ("condition" in action) {
return "check_condition";
}
if ("event" in action) {
return "fire_event";
}
if ("device_id" in action) {
return "device_action";
}
if ("scene" in action) {
return "activate_scene";
}
if ("repeat" in action) {
return "repeat";
}
if ("choose" in action) {
return "choose";
}
if ("wait_for_trigger" in action) {
return "wait_for_trigger";
}
if ("variables" in action) {
return "variables";
}
if ("service" in action) {
return "service";
}
return "unknown";
};
-8
View File
@@ -1,8 +0,0 @@
import { HomeAssistant } from "../types";
import { Action } from "./script";
export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) =>
hass.callWS({
type: "execute_script",
sequence,
});
-157
View File
@@ -1,157 +0,0 @@
import { strStartsWith } from "../common/string/starts-with";
import { HomeAssistant, Context } from "../types";
import { AutomationConfig } from "./automation";
interface BaseTraceStep {
path: string;
timestamp: string;
changed_variables?: Record<string, unknown>;
}
export interface TriggerTraceStep extends BaseTraceStep {
changed_variables: {
trigger: {
description: string;
[key: string]: unknown;
};
[key: string]: unknown;
};
}
export interface ConditionTraceStep extends BaseTraceStep {
result: { result: boolean };
}
export interface CallServiceActionTraceStep extends BaseTraceStep {
result: {
limit: number;
running_script: boolean;
params: Record<string, unknown>;
};
child_id?: {
domain: string;
item_id: string;
run_id: string;
};
}
export interface ChooseActionTraceStep extends BaseTraceStep {
result: { choice: number | "default" };
}
export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
result: { result: boolean };
}
export type ActionTraceStep =
| BaseTraceStep
| ConditionTraceStep
| CallServiceActionTraceStep
| ChooseActionTraceStep
| ChooseChoiceActionTraceStep;
export interface AutomationTrace {
domain: string;
item_id: string;
last_action: string | null;
last_condition: string | null;
run_id: string;
state: "running" | "stopped" | "debugged";
timestamp: {
start: string;
finish: string | null;
};
trigger: unknown;
}
export interface AutomationTraceExtended extends AutomationTrace {
trace: Record<string, ActionTraceStep[]>;
context: Context;
variables: Record<string, unknown>;
config: AutomationConfig;
}
interface TraceTypes {
automation: {
short: AutomationTrace;
extended: AutomationTraceExtended;
};
}
export const loadTrace = <T extends keyof TraceTypes>(
hass: HomeAssistant,
domain: T,
item_id: string,
run_id: string
): Promise<TraceTypes[T]["extended"]> =>
hass.callWS({
type: "trace/get",
domain,
item_id,
run_id,
});
export const loadTraces = <T extends keyof TraceTypes>(
hass: HomeAssistant,
domain: T,
item_id: string
): Promise<Array<TraceTypes[T]["short"]>> =>
hass.callWS({
type: "trace/list",
domain,
item_id,
});
export type TraceContexts = Record<
string,
{ run_id: string; domain: string; item_id: string }
>;
export const loadTraceContexts = (
hass: HomeAssistant,
domain?: string,
item_id?: string
): Promise<TraceContexts> =>
hass.callWS({
type: "trace/contexts",
domain,
item_id,
});
export const getDataFromPath = (
config: AutomationConfig,
path: string
): any => {
const parts = path.split("/").reverse();
let result: any = config;
while (parts.length) {
const raw = parts.pop()!;
const asNumber = Number(raw);
if (isNaN(asNumber)) {
result = result[raw];
continue;
}
if (Array.isArray(result)) {
result = result[asNumber];
continue;
}
if (asNumber !== 0) {
throw new Error("If config is not an array, can only return index 0");
}
}
return result;
};
// It is 'trigger' if manually triggered by the user via UI
export const isTriggerPath = (path: string): boolean =>
path === "trigger" || strStartsWith(path, "trigger/");
export const getTriggerPathFromTrace = (
steps: Record<string, BaseTraceStep[]>
): string | undefined => Object.keys(steps).find((path) => isTriggerPath(path));
-10
View File
@@ -1,18 +1,8 @@
import { HomeAssistant } from "../types";
import { fetchFrontendUserData, saveFrontendUserData } from "./frontend";
export enum NumberFormat {
language = "language",
system = "system",
comma_decimal = "comma_decimal",
decimal_comma = "decimal_comma",
space_comma = "space_comma",
none = "none",
}
export interface FrontendTranslationData {
language: string;
number_format: NumberFormat;
}
declare global {
+2 -2
View File
@@ -133,7 +133,7 @@ export const getWind = (
speed: string,
bearing: string
): string => {
const speedText = `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
const speedText = `${formatNumber(speed, hass!.language)} ${getWeatherUnit(
hass!,
"wind_speed"
)}`;
@@ -206,7 +206,7 @@ export const getSecondaryWeatherAttribute = (
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, attribute)}
`;
};
+6 -11
View File
@@ -82,17 +82,12 @@ export interface ZHAGroupMember {
export const reconfigureNode = (
hass: HomeAssistant,
ieeeAddress: string,
callbackFunction: any
) => {
return hass.connection.subscribeMessage(
(message) => callbackFunction(message),
{
type: "zha/devices/reconfigure",
ieee: ieeeAddress,
}
);
};
ieeeAddress: string
): Promise<void> =>
hass.callWS({
type: "zha/devices/reconfigure",
ieee: ieeeAddress,
});
export const refreshTopology = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
-58
View File
@@ -28,34 +28,6 @@ export interface ZWaveJSNode {
status: number;
}
export interface ZWaveJSNodeConfigParams {
property: number;
value: any;
configuration_value_type: string;
metadata: ZWaveJSNodeConfigParamMetadata;
}
export interface ZWaveJSNodeConfigParamMetadata {
description: string;
label: string;
max: number;
min: number;
readable: boolean;
writeable: boolean;
type: string;
unit: string;
states: { [key: number]: string };
}
export interface ZWaveJSSetConfigParamData {
type: string;
entry_id: string;
node_id: number;
property: number;
property_key?: number;
value: string | number;
}
export enum NodeStatus {
Unknown,
Asleep,
@@ -86,36 +58,6 @@ export const fetchNodeStatus = (
node_id,
});
export const fetchNodeConfigParameters = (
hass: HomeAssistant,
entry_id: string,
node_id: number
): Promise<ZWaveJSNodeConfigParams[]> =>
hass.callWS({
type: "zwave_js/get_config_parameters",
entry_id,
node_id,
});
export const setNodeConfigParameter = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
property: number,
value: number,
property_key?: number
): Promise<unknown> => {
const data: ZWaveJSSetConfigParamData = {
type: "zwave_js/set_config_parameter",
entry_id,
node_id,
property,
value,
property_key,
};
return hass.callWS(data);
};
export const getIdentifiersFromDevice = function (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined {
@@ -315,7 +315,7 @@ class DataEntryFlowDialog extends LitElement {
this._step.type === "create_entry"
) {
if (this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchDevices(this._step.result);
this._fetchAreas();
} else {
this._devices = [];
@@ -43,13 +43,6 @@ class StepFlowCreateEntry extends LitElement {
<h2>Success!</h2>
<div class="content">
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
${this.step.result.state === "not_loaded"
? html`<span class="error"
>${localize(
"ui.panel.config.integrations.config_flow.not_loaded"
)}</span
>`
: ""}
${this.devices.length === 0
? ""
: html`
@@ -143,9 +136,6 @@ class StepFlowCreateEntry extends LitElement {
width: 100%;
}
}
.error {
color: var(--error-color);
}
`,
];
}
+4 -4
View File
@@ -36,12 +36,12 @@ class DialogBox extends LitElement {
public closeDialog(): boolean {
if (this._params?.confirmation || this._params?.prompt) {
return false;
}
if (this._params) {
this._dismiss();
return true;
}
if (this._params) {
return false;
}
return true;
}
@@ -140,7 +140,7 @@ class DialogBox extends LitElement {
}
private _dialogClosed(ev) {
if (ev.detail.action === "ignore") {
if (this._params?.prompt && ev.detail.action === "ignore") {
return;
}
this._dismiss();
@@ -50,7 +50,7 @@ class MoreInfoSun extends LitElement {
<div class="value">
${formatTime(
item === "ris" ? risingDate : settingDate,
this.hass.locale
this.hass.language
)}
</div>
</div>
@@ -61,7 +61,10 @@ class MoreInfoSun extends LitElement {
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div>
<div class="value">
${formatNumber(this.stateObj.attributes.elevation, this.hass.locale)}
${formatNumber(
this.stateObj.attributes.elevation,
this.hass!.language
)}
</div>
</div>
`;
@@ -123,7 +123,7 @@ class MoreInfoVacuum extends LitElement {
<div>
<span>
<ha-icon .icon=${stateObj.attributes.battery_icon}></ha-icon>
${stateObj.attributes.battery_level} %
${stateObj.attributes.battery_level}%
</span>
</div>
`
@@ -68,7 +68,7 @@ class MoreInfoWeather extends LitElement {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!oldHass ||
oldHass.locale !== this.hass.locale ||
oldHass.language !== this.hass.language ||
oldHass.config.unit_system !== this.hass.config.unit_system
) {
return true;
@@ -91,7 +91,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
this.hass!.language
)}
${getWeatherUnit(this.hass, "temperature")}
</div>
@@ -106,7 +106,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.pressure,
this.hass.locale
this.hass!.language
)}
${getWeatherUnit(this.hass, "air_pressure")}
</div>
@@ -123,7 +123,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
this.hass!.language
)}
%
</div>
@@ -157,7 +157,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.visibility,
this.hass.locale
this.hass!.language
)}
${getWeatherUnit(this.hass, "length")}
</div>
@@ -184,7 +184,7 @@ class MoreInfoWeather extends LitElement {
<div class="main">
${formatTimeWeekday(
new Date(item.datetime),
this.hass.locale
this.hass.language
)}
</div>
`
@@ -194,17 +194,17 @@ class MoreInfoWeather extends LitElement {
<div class="main">
${formatDateWeekday(
new Date(item.datetime),
this.hass.locale
this.hass.language
)}
</div>
<div class="templow">
${formatNumber(item.templow, this.hass.locale)}
${formatNumber(item.templow, this.hass!.language)}
${getWeatherUnit(this.hass, "temperature")}
</div>
`
: ""}
<div class="temp">
${formatNumber(item.temperature, this.hass.locale)}
${formatNumber(item.temperature, this.hass!.language)}
${getWeatherUnit(this.hass, "temperature")}
</div>
</div>
+7 -21
View File
@@ -9,12 +9,10 @@ import {
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { closeDialog } from "../make-dialog-manager";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { TraceContexts, loadTraceContexts } from "../../data/trace";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
@@ -28,8 +26,6 @@ export class MoreInfoLogbook extends LitElement {
@internalProperty() private _logbookEntries?: LogbookEntry[];
@internalProperty() private _traceContexts?: TraceContexts;
@internalProperty() private _persons = {};
private _lastLogbookDate?: Date;
@@ -67,7 +63,6 @@ export class MoreInfoLogbook extends LitElement {
relative-time
.hass=${this.hass}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._persons}
></ha-logbook>
`
@@ -80,11 +75,6 @@ export class MoreInfoLogbook extends LitElement {
protected firstUpdated(): void {
this._fetchPersonNames();
this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
}
});
}
protected updated(changedProps: PropertyValues): void {
@@ -125,21 +115,17 @@ export class MoreInfoLogbook extends LitElement {
this._lastLogbookDate ||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
const [newEntries, traceContexts] = await Promise.all([
getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
),
loadTraceContexts(this.hass),
]);
const newEntries = await getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
);
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
: newEntries;
this._lastLogbookDate = now;
this._traceContexts = traceContexts;
}
private _fetchPersonNames() {
@@ -42,7 +42,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
>${computeStateDisplay(
this.hass.localize,
this.notification,
this.hass.locale
this.hass.language
)}</mwc-button
>
</notification-item-template>
@@ -20,7 +20,7 @@ export class HuiNotificationDrawer extends EventsMixin(
) {
static get template() {
return html`
<style>
<style include="paper-material-styles">
app-toolbar {
color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color);
@@ -9,7 +9,6 @@ import {
property,
TemplateResult,
} from "lit-element";
import { formatDateTime } from "../../common/datetime/format_date_time";
import "../../components/ha-markdown";
import "../../components/ha-relative-time";
import { PersistentNotification } from "../../data/persistent_notification";
@@ -93,7 +92,13 @@ export class HuiPersistentNotificationItem extends LitElement {
}
const d = new Date(notification.created_at!);
return formatDateTime(d, hass.locale);
return d.toLocaleDateString(hass.language, {
year: "numeric",
month: "short",
day: "numeric",
minute: "numeric",
hour: "numeric",
});
}
}
+80 -189
View File
@@ -3,7 +3,7 @@ import type { List } from "@material/mwc-list/mwc-list";
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import type { ListItem } from "@material/mwc-list/mwc-list-item";
import { mdiConsoleLine, mdiEarth, mdiReload, mdiServerNetwork } from "@mdi/js";
import { mdiConsoleLine } from "@mdi/js";
import {
css,
customElement,
@@ -36,7 +36,7 @@ import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-header-bar";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
import { getPanelIcon, getPanelNameTranslationKey } from "../../data/panel";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { configSections } from "../../panels/config/ha-panel-config";
import { haStyleDialog } from "../../resources/styles";
@@ -46,44 +46,31 @@ import {
showConfirmationDialog,
} from "../generic/show-dialog-box";
import { QuickBarParams } from "./show-dialog-quick-bar";
import "../../components/ha-chip";
const DEFAULT_NAVIGATION_ICON = "hass:arrow-right-circle";
const DEFAULT_SERVER_ICON = "hass:server";
interface QuickBarItem extends ScorableTextItem {
primaryText: string;
icon?: string;
iconPath?: string;
action(data?: any): void;
}
interface CommandItem extends QuickBarItem {
categoryKey: "reload" | "navigation" | "server_control";
categoryText: string;
}
interface EntityItem extends QuickBarItem {
icon?: string;
}
const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => {
return (item as CommandItem).categoryKey !== undefined;
};
interface QuickBarNavigationItem extends CommandItem {
interface QuickBarNavigationItem extends QuickBarItem {
path: string;
}
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
interface NavigationInfo extends PageNavigation {
text: string;
}
type BaseNavigationCommand = Pick<
QuickBarNavigationItem,
"primaryText" | "path"
>;
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _commandItems?: CommandItem[];
@internalProperty() private _commandItems?: QuickBarItem[];
@internalProperty() private _entityItems?: EntityItem[];
@internalProperty() private _entityItems?: QuickBarItem[];
@internalProperty() private _items?: QuickBarItem[] = [];
@@ -214,12 +201,6 @@ export class QuickBar extends LitElement {
}
private _renderItem(item: QuickBarItem, index?: number) {
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item, index);
}
private _renderEntityItem(item: EntityItem, index?: number) {
return html`
<mwc-list-item
.twoline=${Boolean(item.altText)}
@@ -227,55 +208,15 @@ export class QuickBar extends LitElement {
hasMeta
index=${ifDefined(index)}
graphic="icon"
class=${this._commandMode ? "command-item" : ""}
>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
class="entity"
slot="graphic"
></ha-svg-icon>`
: html`<ha-icon
.icon=${item.icon}
class="entity"
slot="graphic"
></ha-icon>`}
<span>${item.primaryText}</span>
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: null}
</mwc-list-item>
`;
}
private _renderCommandItem(item: CommandItem, index?: number) {
return html`
<mwc-list-item
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
class="command-item"
>
<span>
<ha-chip
.label="${item.categoryText}"
hasIcon
class="command-category ${item.categoryKey}"
>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
slot="icon"
></ha-svg-icon>`
: ""}
${item.categoryText}</ha-chip
>
</span>
<span class="command-text">${item.primaryText}</span>
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
${item.text}
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
@@ -374,112 +315,94 @@ export class QuickBar extends LitElement {
private _generateEntityItems(): QuickBarItem[] {
return Object.keys(this.hass.states)
.map((entityId) => {
const primaryText = computeStateName(this.hass.states[entityId]);
return {
primaryText,
filterText: primaryText,
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
})
.sort((a, b) =>
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
);
.map((entityId) => ({
text: computeStateName(this.hass.states[entityId]),
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
}))
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
}
private _generateCommandItems(): CommandItem[] {
private _generateCommandItems(): QuickBarItem[] {
return [
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) =>
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
);
].sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
}
private _generateReloadCommands(): CommandItem[] {
private _generateReloadCommands(): QuickBarItem[] {
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
return reloadableDomains.map((domain) => {
const categoryText = this.hass.localize(
`ui.dialogs.quick-bar.commands.types.reload`
);
const primaryText =
return reloadableDomains.map((domain) => ({
text:
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload",
"domain",
domainToName(this.hass.localize, domain)
);
return {
primaryText,
filterText: `${categoryText} ${primaryText}`,
action: () => this.hass.callService(domain, "reload"),
categoryKey: "reload",
iconPath: mdiReload,
categoryText,
};
});
),
icon: domainIcon(domain),
action: () => this.hass.callService(domain, "reload"),
}));
}
private _generateServerControlCommands(): CommandItem[] {
private _generateServerControlCommands(): QuickBarItem[] {
const serverActions = ["restart", "stop"];
return serverActions.map((action) => {
const categoryKey = "server_control";
const categoryText = this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
);
const primaryText = this.hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
"action",
this.hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
)
);
return this._generateConfirmationCommand(
return serverActions.map((action) =>
this._generateConfirmationCommand(
{
primaryText,
filterText: `${categoryText} ${primaryText}`,
categoryKey,
iconPath: mdiServerNetwork,
categoryText,
text: this.hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
"action",
this.hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
)
),
icon: DEFAULT_SERVER_ICON,
action: () => this.hass.callService("homeassistant", action),
},
this.hass.localize("ui.dialogs.generic.ok")
);
});
)
);
}
private _generateNavigationCommands(): CommandItem[] {
private _generateNavigationCommands(): QuickBarItem[] {
const panelItems = this._generateNavigationPanelCommands();
const sectionItems = this._generateNavigationConfigSectionCommands();
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
return this._withNavigationActions([...panelItems, ...sectionItems]);
}
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
private _generateNavigationPanelCommands(): Omit<
QuickBarNavigationItem,
"action"
>[] {
return Object.keys(this.hass.panels)
.filter((panelKey) => panelKey !== "_my_redirect")
.map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const primaryText =
this.hass.localize(translationKey) || panel.title || panel.url_path;
const text = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
"panel",
this.hass.localize(translationKey) || panel.title || panel.url_path
);
return {
primaryText,
text,
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
path: `/${panel.url_path}`,
};
});
}
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
private _generateNavigationConfigSectionCommands(): Partial<
QuickBarNavigationItem
>[] {
const items: NavigationInfo[] = [];
for (const sectionKey of Object.keys(configSections)) {
@@ -503,12 +426,18 @@ export class QuickBar extends LitElement {
page: PageNavigation
): NavigationInfo | undefined {
if (page.component) {
const caption = this.hass.localize(
const shortCaption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
if (page.translationKey && caption) {
return { ...page, primaryText: caption };
if (page.translationKey && shortCaption) {
const caption = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
"panel",
shortCaption
);
return { ...page, text: caption };
}
}
@@ -516,9 +445,9 @@ export class QuickBar extends LitElement {
}
private _generateConfirmationCommand(
item: CommandItem,
item: QuickBarItem,
confirmText: ConfirmationDialogParams["confirmText"]
): CommandItem {
): QuickBarItem {
return {
...item,
action: () =>
@@ -529,24 +458,13 @@ export class QuickBar extends LitElement {
};
}
private _finalizeNavigationCommands(
items: BaseNavigationCommand[]
): CommandItem[] {
return items.map((item) => {
const categoryKey = "navigation";
const categoryText = this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
);
return {
...item,
categoryKey,
iconPath: mdiEarth,
categoryText,
filterText: `${categoryText} ${item.primaryText}`,
action: () => navigate(this, item.path),
};
});
private _withNavigationActions(items) {
return items.map(({ text, icon, iconPath, path }) => ({
text,
icon,
iconPath,
action: () => navigate(this, path),
}));
}
private _toggleIfAlreadyOpened() {
@@ -588,8 +506,8 @@ export class QuickBar extends LitElement {
}
}
ha-icon.entity,
ha-svg-icon.entity {
ha-icon,
ha-svg-icon {
margin-left: 20px;
}
@@ -598,33 +516,6 @@ export class QuickBar extends LitElement {
color: var(--primary-text-color);
}
span.command-category {
font-weight: bold;
padding: 3px;
display: inline-flex;
border-radius: 6px;
color: black;
}
.command-category.reload {
--ha-chip-background-color: #cddc39;
--ha-chip-text-color: black;
}
.command-category.navigation {
--ha-chip-background-color: var(--light-primary-color);
--ha-chip-text-color: black;
}
.command-category.server_control {
--ha-chip-background-color: var(--warning-color);
--ha-chip-text-color: black;
}
span.command-text {
margin-left: 8px;
}
.uni-virtualizer-host {
display: block;
position: relative;
+1 -20
View File
@@ -9,7 +9,6 @@ import "../resources/safari-14-attachshadow-patch";
import { createCustomPanelElement } from "../util/custom-panel/create-custom-panel-element";
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
import { baseEntrypointStyles } from "../resources/styles";
declare global {
interface Window {
@@ -42,7 +41,6 @@ function initialize(
properties: Record<string, unknown>
) {
const style = document.createElement("style");
style.innerHTML = "body{margin:0}";
document.head.appendChild(style);
@@ -88,24 +86,7 @@ function initialize(
(err) => {
// eslint-disable-next-line
console.error(err, panel);
let errorScreen;
if (panel.url_path === "hassio") {
import("../layouts/supervisor-error-screen");
errorScreen = document.createElement(
"supervisor-error-screen"
) as any;
} else {
import("../layouts/hass-error-screen");
errorScreen = document.createElement("hass-error-screen") as any;
errorScreen.error = `Unable to load the panel source: ${err}.`;
}
const errorStyle = document.createElement("style");
errorStyle.innerHTML = baseEntrypointStyles.cssText;
document.body.appendChild(errorStyle);
errorScreen.hass = properties.hass;
document.body.appendChild(errorScreen);
alert(`Unable to load the panel source: ${err}.`);
}
);
}
+6 -41
View File
@@ -2,19 +2,15 @@
// eslint-disable-next-line spaced-comment
/// <reference path="../types/service-worker.d.ts" />
/* eslint-env serviceworker */
import { cacheNames, RouteHandler } from "workbox-core";
import { cacheNames } from "workbox-core";
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { registerRoute, setCatchHandler } from "workbox-routing";
import { registerRoute } from "workbox-routing";
import {
CacheFirst,
NetworkOnly,
StaleWhileRevalidate,
} from "workbox-strategies";
const noFallBackRegEx = new RegExp(
`${location.host}/(api|static|auth|frontend_latest|frontend_es5|local)/.*`
);
// Clean up caches from older workboxes and old service workers.
// Will help with cleaning up Workbox v4 stuff
cleanupOutdatedCaches();
@@ -22,17 +18,13 @@ cleanupOutdatedCaches();
function initRouting() {
precacheAndRoute(
// @ts-ignore
WB_MANIFEST,
{
// Ignore all URL parameters.
ignoreURLParametersMatching: [/.*/],
}
WB_MANIFEST
);
// Cache static content (including translations) on first access.
registerRoute(
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
new CacheFirst({ matchOptions: { ignoreSearch: true } })
new CacheFirst()
);
// Get api from network.
@@ -49,14 +41,8 @@ function initRouting() {
new NetworkOnly()
);
// For the root "/" we ignore search
registerRoute(
new RegExp(`^${location.host}/(\\?.*)?$`),
new StaleWhileRevalidate({ matchOptions: { ignoreSearch: true } })
);
// For rest of the files (on Home Assistant domain only) try both cache and network.
// This includes "/states" response and user files from "/local".
// This includes the root "/" or "/states" response and user files from "/local".
// First access might bring stale data from cache, but a single refresh will bring updated
// file.
registerRoute(new RegExp(`${location.host}/.*`), new StaleWhileRevalidate());
@@ -172,15 +158,8 @@ function initPushNotifications() {
self.addEventListener("install", (event) => {
// Delete all runtime caching, so that index.html has to be refetched.
// And add the new index.html back to the runtime cache
const cacheName = cacheNames.runtime;
event.waitUntil(
caches.delete(cacheName).then(() =>
caches.open(cacheName).then((cache) => {
cache.add("/");
})
)
);
event.waitUntil(caches.delete(cacheName));
});
self.addEventListener("activate", () => {
@@ -198,19 +177,5 @@ self.addEventListener("message", (message) => {
}
});
const catchHandler: RouteHandler = async (options) => {
const dest = (options.request as Request).destination;
const url = (options.request as Request).url;
if (dest !== "document" || noFallBackRegEx.test(url)) {
return Response.error();
}
// eslint-disable-next-line no-console
console.log("Using fallback for:", url);
return (await caches.match("/", { ignoreSearch: true })) || Response.error();
};
initRouting();
setCatchHandler(catchHandler);
initPushNotifications();
+1 -5
View File
@@ -5,7 +5,6 @@ import {
} from "../common/dom/apply_themes_on_element";
import { computeLocalize } from "../common/translations/localize";
import { DEFAULT_PANEL } from "../data/panel";
import { NumberFormat } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getTranslation, getLocalLanguage } from "../util/hass-translation";
@@ -199,12 +198,9 @@ export const provideHass = (
},
panelUrl: "lovelace",
defaultPanel: DEFAULT_PANEL,
language: localLanguage,
selectedLanguage: localLanguage,
locale: {
language: localLanguage,
number_format: NumberFormat.language,
},
resources: null as any,
localize: () => "",
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<head>
<title>Home Assistant</title>
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<style>
.content {
+2 -2
View File
@@ -2,8 +2,8 @@
<html>
<head>
<% if (!useWDS) { %>
<link rel="modulepreload" href="<%= latestCoreJS %>" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestAppJS %>" crossorigin="use-credentials" />
<link rel="preload" href="<%= latestCoreJS %>" as="script" crossorigin="use-credentials" />
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
<% } %>
<%= renderTemplate('_header') %>
<title>Home Assistant</title>
+19 -35
View File
@@ -2,21 +2,29 @@
<html>
<head>
<title>Home Assistant</title>
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<style>
html {
color: var(--primary-text-color, #212121);
background-color: #0277bd !important;
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
color: #e1e1e1;
}
ha-onboarding {
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
}
}
.content {
box-sizing: border-box;
padding: 20px 16px;
border-radius: 4px;
max-width: 432px;
margin: 64px auto 0;
box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;
background-color: #fff;
max-width: 400px;
margin: 0 auto;
}
.header {
@@ -31,36 +39,12 @@
.header img {
margin-right: 16px;
}
@media (prefers-color-scheme: dark) {
html {
color: #e1e1e1;
}
ha-onboarding {
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
}
.content {
background-color: #111111;
}
}
@media (max-width: 450px) {
.content {
min-height: 100%;
margin: 0;
}
}
</style>
</head>
<body id='particles'>
<body>
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" width="52" />
<img src="/static/icons/favicon-192x192.png" height="52" />
Home Assistant
</div>
@@ -92,4 +76,4 @@
})();
</script>
</body>
</html>
</html>
+6 -22
View File
@@ -9,8 +9,7 @@ import {
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../types";
import "../components/ha-menu-button";
import "../components/ha-icon-button-arrow-prev";
import "./hass-subpage";
@customElement("hass-error-screen")
class HassErrorScreen extends LitElement {
@@ -18,37 +17,22 @@ class HassErrorScreen extends LitElement {
@property({ type: Boolean }) public toolbar = true;
@property({ type: Boolean }) public rootnav = false;
@property() public narrow?: boolean;
@property() public error?: string;
protected render(): TemplateResult {
return html`
${this.toolbar
? html`<div class="toolbar">
${this.rootnav
? html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
</div>`
: ""}
<div class="content">
<h3>${this.error}</h3>
<slot>
<mwc-button @click=${this._handleBack}>
${this.hass?.localize("ui.panel.error.go_back") || "go back"}
</mwc-button>
<mwc-button @click=${this._handleBack}>go back</mwc-button>
</slot>
</div>
`;

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