Compare commits

..

3 Commits

Author SHA1 Message Date
Paulus Schoutsen
d011794884 Bumped version to 20220502.0 2022-05-02 15:07:50 -07:00
Bram Kragten
9b782cabe2 Update hat-script-graph.ts 2022-05-02 15:07:15 -07:00
Bram Kragten
a355bedd77 Indicate things are disabled in trace graph 2022-05-02 15:07:15 -07:00
262 changed files with 3861 additions and 8123 deletions

View File

@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!-- <!--
Provide details about the versions you are using, which helps us reproducing Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the and finding the issue quicker. Version information is found in the
Home Assistant frontend: Settings -> About. Home Assistant frontend: Configuration -> Info.
Browser version and operating system is important! Please try to replicate Browser version and operating system is important! Please try to replicate
your issue in a different browser and be sure to include your findings. your issue in a different browser and be sure to include your findings.

View File

@@ -1,4 +1,4 @@
name: Report a bug with the UI / Dashboards name: Report a bug with the UI, Frontend or Lovelace
description: Report an issue related to the Home Assistant frontend. description: Report an issue related to the Home Assistant frontend.
labels: bug labels: bug
body: body:
@@ -9,7 +9,7 @@ body:
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
**Please not not report issues for custom cards.** **Please not not report issues for custom Lovelace cards.**
[fr]: https://github.com/home-assistant/frontend/discussions [fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases [releases]: https://github.com/home-assistant/home-assistant/releases
@@ -64,7 +64,7 @@ body:
label: What version of Home Assistant Core has the issue? label: What version of Home Assistant Core has the issue?
placeholder: core- placeholder: core-
description: > description: >
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/). Can be found in the Configuration panel -> Info.
- type: input - type: input
attributes: attributes:
label: What was the last working version of Home Assistant Core? label: What was the last working version of Home Assistant Core?

View File

@@ -1,17 +1,17 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Request a feature for the UI / Dashboards - name: Request a feature for the UI, Frontend or Lovelace
url: https://github.com/home-assistant/frontend/discussions/category_choices url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend. about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI / Dashboards - name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/core/issues url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository. about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
- name: Report incorrect or missing information on our website - name: Report incorrect or missing information on our website
url: https://github.com/home-assistant/home-assistant.io/issues url: https://github.com/home-assistant/home-assistant.io/issues
about: Our documentation has its own issue tracker. Please report issues with the website there. about: Our documentation has its own issue tracker. Please report issues with the website there.
- name: I have a question or need support - name: I have a question or need support
url: https://www.home-assistant.io/help url: https://www.home-assistant.io/help
about: We use GitHub for tracking bugs. Check our website for resources on getting help. about: We use GitHub for tracking bugs, check our website for resources on getting help.
- name: I'm unsure where to go - name: I'm unsure where to go
url: https://www.home-assistant.io/join-chat url: https://www.home-assistant.io/join-chat
about: If you are unsure where to go, then joining our chat is recommended; Just ask! about: If you are unsure where to go, then joining our chat is recommended; Just ask!

2
.vscode/tasks.json vendored
View File

@@ -181,7 +181,7 @@
{ {
"label": "Run HA Core for Supervisor in devcontainer", "label": "Run HA Core for Supervisor in devcontainer",
"type": "shell", "type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core", "command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
"isBackground": true, "isBackground": true,
"group": { "group": {
"kind": "build", "kind": "build",

View File

@@ -26,8 +26,8 @@ module.exports = {
}, },
version() { version() {
const version = fs const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8") .readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d)"/); .match(/version\W+=\W(\d{8}\.\d)/);
if (!version) { if (!version) {
throw Error("Version not found"); throw Error("Version not found");
} }

View File

@@ -1,9 +0,0 @@
# These redirects are handled by Netlify
#
# Some custom cards are not prefixing the instance URL when fetching data
# and can end up fetching the data from the Cast domain instead of HA.
# This will make sure that some common ones are replaced with a placeholder.
/api/camera_proxy/* /images/google-nest-hub.png
/api/camera_proxy_stream/* /images/google-nest-hub.png
/api/media_player_proxy/* /images/google-nest-hub.png

View File

@@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
type: "state-icon", type: "state-icon",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "group.downstairs_lights", entity_id: "group.downstairs_lights",
}, },
service: "homeassistant.toggle", service: "homeassistant.toggle",

View File

@@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_quiet", entity_id: "script.air_cleaner_quiet",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_auto", entity_id: "script.air_cleaner_auto",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_turbo", entity_id: "script.air_cleaner_turbo",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_off", entity_id: "script.ac_off",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_on", entity_id: "script.ac_on",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.morning_lights", entity: "scene.morning_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.morning_lights", entity_id: "scene.morning_lights",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.movie_time", entity: "scene.movie_time",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.movie_time", entity_id: "scene.movie_time",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.downstairs_lights", entity: "light.downstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.downstairs_lights", entity_id: "light.downstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",
@@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.upstairs_lights", entity: "light.upstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.upstairs_lights", entity_id: "light.upstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; import { format, startOfToday, startOfTomorrow } from "date-fns";
import { EnergySolarForecasts } from "../../../src/data/energy"; import { EnergySolarForecasts } from "../../../src/data/energy";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";

View File

@@ -4,7 +4,7 @@ import {
addMonths, addMonths,
differenceInHours, differenceInHours,
endOfDay, endOfDay,
} from "date-fns/esm"; } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history"; import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";

View File

@@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_2"], entity_id: ["input_boolean.toggle_2"],
}, },
@@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_3"], entity_id: ["input_boolean.toggle_3"],
}, },
@@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -298,11 +298,11 @@ export const basicTrace: DemoTrace = {
source: "state of input_boolean.toggle_1", source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles", entity_id: "automation.toggle_toggles",
context_id: "6cfcae368e7b3686fad6c59e83ae76c9", context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
when: 1616647011.240832, when: "2021-03-25T04:36:51.240832+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1616647011.249828, when: "2021-03-25T04:36:51.249828+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "on", state: "on",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
@@ -313,7 +313,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.258947, when: "2021-03-25T04:36:51.258947+00:00",
name: "Toggle 2", name: "Toggle 2",
state: "on", state: "on",
entity_id: "input_boolean.toggle_2", entity_id: "input_boolean.toggle_2",
@@ -324,7 +324,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.261806, when: "2021-03-25T04:36:51.261806+00:00",
name: "Toggle 3", name: "Toggle 3",
state: "off", state: "off",
entity_id: "input_boolean.toggle_3", entity_id: "input_boolean.toggle_3",
@@ -335,7 +335,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.265246, when: "2021-03-25T04:36:51.265246+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "off", state: "off",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",

View File

@@ -185,11 +185,11 @@ export const motionLightTrace: DemoTrace = {
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use", "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", source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato", entity_id: "automation.auto_elgato",
when: 1615702021.768492, when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1615702021.872187, when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "on", state: "on",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",
@@ -200,7 +200,7 @@ export const motionLightTrace: DemoTrace = {
context_name: "Auto Elgato", context_name: "Auto Elgato",
}, },
{ {
when: 1615702073.284505, when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "off", state: "off",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",

View File

@@ -249,7 +249,7 @@ const CONFIGS = [
name: Bed light name: Bed light
action_name: Toggle light action_name: Toggle light
service: light.toggle service: light.toggle
data: service_data:
entity_id: light.bed_light entity_id: light.bed_light
- type: section - type: section
label: Links label: Links

View File

@@ -199,7 +199,7 @@ const CONFIGS = [
tap_action: tap_action:
action: call-service action: call-service
service: light.turn_on service: light.turn_on
data: service_data:
entity_id: light.ceiling_lights entity_id: light.ceiling_lights
- entity: sun.sun - entity: sun.sun
name: Regular name: Regular

View File

@@ -40,7 +40,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv
@@ -88,7 +88,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv

View File

@@ -68,7 +68,6 @@ class HassioAddonRepositoryEl extends LitElement {
${addons.map( ${addons.map(
(addon) => html` (addon) => html`
<ha-card <ha-card
outlined
.addon=${addon} .addon=${addon}
class=${addon.available ? "" : "not_available"} class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped} @click=${this._addonTapped}

View File

@@ -50,7 +50,6 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("addon.configuration.audio.header")} .header=${this.supervisor.localize("addon.configuration.audio.header")}
> >
<div class="card-content"> <div class="card-content">

View File

@@ -162,7 +162,7 @@ class HassioAddonConfig extends LitElement {
); );
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card outlined> <ha-card>
<div class="header"> <div class="header">
<h2> <h2>
${this.supervisor.localize("addon.configuration.options.header")} ${this.supervisor.localize("addon.configuration.options.header")}

View File

@@ -58,7 +58,6 @@ class HassioAddonNetwork extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize( .header=${this.supervisor.localize(
"addon.configuration.network.header" "addon.configuration.network.header"
)} )}

View File

@@ -38,7 +38,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
} }
return html` return html`
<div class="content"> <div class="content">
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -17,10 +17,7 @@ import {
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
fetchHassioSupervisorInfo,
setSupervisorOption,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen"; import "../../../src/layouts/hass-error-screen";
@@ -172,40 +169,38 @@ class HassioAddonDashboard extends LitElement {
if (this.route.path === "") { if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon"); const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url"); const requestedAddonRepository = extractSearchParam("repository_url");
if (requestedAddonRepository) { if (
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass); requestedAddonRepository &&
!this.supervisor.supervisor.addons_repositories.find(
(repo) => repo === requestedAddonRepository
)
) {
if ( if (
!supervisorInfo.addons_repositories.find( !(await showConfirmationDialog(this, {
(repo) => repo === requestedAddonRepository title: this.supervisor.localize("my.add_addon_repository_title"),
) text: this.supervisor.localize(
"my.add_addon_repository_description",
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) { ) {
if ( this._error = this.supervisor.localize(
!(await showConfirmationDialog(this, { "my.error_repository_not_found"
title: this.supervisor.localize("my.add_addon_repository_title"), );
text: this.supervisor.localize( return;
"my.add_addon_repository_description", }
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) {
this._error = this.supervisor.localize(
"my.error_repository_not_found"
);
return;
}
try { try {
await setSupervisorOption(this.hass, { await setSupervisorOption(this.hass, {
addons_repositories: [ addons_repositories: [
...supervisorInfo.addons_repositories, ...this.supervisor.supervisor.addons_repositories,
requestedAddonRepository, requestedAddonRepository,
], ],
}); });
} catch (err: any) { } catch (err: any) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
}
} }
} }

View File

@@ -166,7 +166,7 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="addon-header"> <div class="addon-header">
${!this.narrow ? this.addon.name : ""} ${!this.narrow ? this.addon.name : ""}
@@ -649,7 +649,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.long_description ${this.addon.long_description
? html` ? html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<ha-markdown <ha-markdown
.content=${this.addon.long_description} .content=${this.addon.long_description}

View File

@@ -34,7 +34,7 @@ class HassioAddonLogs extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -26,7 +26,7 @@ class HassioAddons extends LitElement {
<div class="card-group"> <div class="card-group">
${!this.supervisor.supervisor.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<button class="link" @click=${this._openStore}> <button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")} ${this.supervisor.localize("dashboard.no_addons")}
@@ -38,11 +38,7 @@ class HassioAddons extends LitElement {
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.map( .map(
(addon) => html` (addon) => html`
<ha-card <ha-card .addon=${addon} @click=${this._addonTapped}>
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content"> <div class="card-content">
<hassio-card-content <hassio-card-content
.hass=${this.hass} .hass=${this.hass}

View File

@@ -85,7 +85,7 @@ export class HassioUpdate extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="icon"> <div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>

View File

@@ -48,7 +48,7 @@ class HassioCoreInfo extends LitElement {
]; ];
return html` return html`
<ha-card header="Core" outlined> <ha-card header="Core">
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -66,7 +66,7 @@ class HassioHostInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Host" outlined> <ha-card header="Host">
<div class="card-content"> <div class="card-content">
<div> <div>
${this.supervisor.host.features.includes("hostname") ${this.supervisor.host.features.includes("hostname")

View File

@@ -57,7 +57,7 @@ class HassioSupervisorInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Supervisor" outlined> <ha-card header="Supervisor">
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -65,7 +65,7 @@ class HassioSupervisorLog extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -128,7 +128,6 @@ class UpdateAvailableCard extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("update_available.update_name", { .header=${this.supervisor.localize("update_available.update_name", {
name: this._name, name: this._name,
})} })}

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.7.96", "@mdi/js": "6.6.95",
"@mdi/svg": "6.7.96", "@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
@@ -89,8 +89,8 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4", "@thomasloven/round-slider": "0.5.4",
"@vaadin/combo-box": "^23.0.10", "@vaadin/combo-box": "^22.0.4",
"@vaadin/vaadin-themable-mixin": "^23.0.10", "@vaadin/vaadin-themable-mixin": "^22.0.4",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -106,9 +106,10 @@
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"fuzzysort": "^1.2.1",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.1.0", "home-assistant-js-websocket": "^7.0.3",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",

View File

@@ -1,30 +1,3 @@
[build-system] [build-system]
requires = ["setuptools~=62.3", "wheel~=0.37.1"] requires = ["setuptools~=60.5", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220526.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.4.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"
[tool.setuptools]
platforms = ["any"]
zip-safe = false
include-package-data = true
[tool.setuptools.packages.find]
include = ["hass_frontend*"]
[tool.mypy]
python_version = 3.4
show_error_codes = true
strict = true

View File

@@ -50,14 +50,14 @@ async function main(args) {
return; return;
} }
const setup = fs.readFileSync("pyproject.toml", "utf8"); const setup = fs.readFileSync("setup.cfg", "utf8");
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1]; const version = setup.match(/\d{8}\.\d+/)[0];
const newVersion = method(version); const newVersion = method(version);
console.log("Current version:", version); console.log("Current version:", version);
console.log("New version:", newVersion); console.log("New version:", newVersion);
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8"); fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
if (!commit) { if (!commit) {
return; return;

26
setup.cfg Normal file
View File

@@ -0,0 +1,26 @@
[metadata]
name = home-assistant-frontend
version = 20220502.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -1,41 +0,0 @@
const DEFAULT_OWN = true;
// Finds the closest ancestor of an element that has a specific optionally owned property,
// traversing slot and shadow root boundaries until the body element is reached
export const closestWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
if (!element || element === document.body) return null;
element = element.assignedSlot ?? element;
if (element.parentElement) {
element = element.parentElement;
} else {
const root = element.getRootNode();
element = root instanceof ShadowRoot ? root.host : null;
}
if (
own
? Object.prototype.hasOwnProperty.call(element, property)
: element && property in element
)
return element;
return closestWithProperty(element, property, own);
};
// Finds the set of all such ancestors and includes starting element as first in the set
export const ancestorsWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
const ancestors: Set<Element> = new Set();
while (element) {
ancestors.add(element);
element = closestWithProperty(element, property, own);
}
return ancestors;
};

View File

@@ -1,11 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity";
export const computeActiveState = (stateObj: HassEntity): string => { export const computeActiveState = (stateObj: HassEntity): string => {
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
return stateObj.state;
}
const domain = stateObj.entity_id.split(".")[0]; const domain = stateObj.entity_id.split(".")[0];
let state = stateObj.state; let state = stateObj.state;

View File

@@ -2,74 +2,67 @@ import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_PROGRESS,
updateIsInstallingFromAttributes,
} from "../../data/update"; } from "../../data/update";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericFromAttributes } from "../number/format_number"; import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { supportsFeatureFromAttributes } from "./supports-feature"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { computeDomain } from "./compute_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
state?: string state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
entityId: string,
attributes: any,
state: string
): string => { ): string => {
if (state === UNKNOWN || state === UNAVAILABLE) { const compareState = state !== undefined ? state : stateObj.state;
return localize(`state.default.${state}`);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
return localize(`state.default.${compareState}`);
} }
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (isNumericState(stateObj)) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && stateObj.attributes.device_class === "duration" &&
attributes.unit_of_measurement && stateObj.attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement] UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
) { ) {
try { try {
return formatDuration(state, attributes.unit_of_measurement); return formatDuration(
compareState,
stateObj.attributes.unit_of_measurement
);
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
if (attributes.device_class === "monetary") { if (stateObj.attributes.device_class === "monetary") {
try { try {
return formatNumber(state, locale, { return formatNumber(compareState, locale, {
style: "currency", style: "currency",
currency: attributes.unit_of_measurement, currency: stateObj.attributes.unit_of_measurement,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); });
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
return `${formatNumber(state, locale)}${ return `${formatNumber(compareState, locale)}${
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : "" stateObj.attributes.unit_of_measurement
? " " + stateObj.attributes.unit_of_measurement
: ""
}`; }`;
} }
const domain = computeDomain(entityId); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state !== undefined) {
@@ -104,32 +97,36 @@ export const computeStateDisplayFromEntityAttributes = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (attributes.has_date && attributes.has_time) { if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
date = new Date( date = new Date(
attributes.year, stateObj.attributes.year,
attributes.month - 1, stateObj.attributes.month - 1,
attributes.day, stateObj.attributes.day,
attributes.hour, stateObj.attributes.hour,
attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, locale); return formatDateTime(date, locale);
} }
if (attributes.has_date) { if (stateObj.attributes.has_date) {
date = new Date(attributes.year, attributes.month - 1, attributes.day); date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale); return formatDate(date, locale);
} }
if (attributes.has_time) { if (stateObj.attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(attributes.hour, attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return state; return stateObj.state;
} }
} }
if (domain === "humidifier") { if (domain === "humidifier") {
if (state === "on" && attributes.humidity) { if (compareState === "on" && stateObj.attributes.humidity) {
return `${attributes.humidity} %`; return `${stateObj.attributes.humidity} %`;
} }
} }
@@ -139,7 +136,7 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" || domain === "number" ||
domain === "input_number" domain === "input_number"
) { ) {
return formatNumber(state, locale); return formatNumber(compareState, locale);
} }
// state of button is a timestamp // state of button is a timestamp
@@ -147,12 +144,12 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "button" || domain === "button" ||
domain === "input_button" || domain === "input_button" ||
domain === "scene" || domain === "scene" ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
try { try {
return formatDateTime(new Date(state), locale); return formatDateTime(new Date(compareState), locale);
} catch (_err) { } catch (_err) {
return state; return compareState;
} }
} }
@@ -163,28 +160,30 @@ export const computeStateDisplayFromEntityAttributes = (
// When the latest version is skipped, show the latest version // When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date" // When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable" // When update is not available and there is no latest_version show "Unavailable"
return state === "on" return compareState === "on"
? updateIsInstallingFromAttributes(attributes) ? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", { ? localize("ui.card.update.installing_with_progress", {
progress: attributes.in_progress, progress: stateObj.attributes.in_progress,
}) })
: localize("ui.card.update.installing") : localize("ui.card.update.installing")
: attributes.latest_version : stateObj.attributes.latest_version
: attributes.skipped_version === attributes.latest_version : stateObj.attributes.skipped_version ===
? attributes.latest_version ?? localize("state.default.unavailable") stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
return ( return (
// Return device class translation // Return device class translation
(attributes.device_class && (stateObj.attributes.device_class &&
localize( localize(
`component.${domain}.state.${attributes.device_class}.${state}` `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
)) || )) ||
// Return default translation // Return default translation
localize(`component.${domain}.state._.${state}`) || localize(`component.${domain}.state._.${compareState}`) ||
// We don't know! Return the raw state. // We don't know! Return the raw state.
state compareState
); );
}; };

View File

@@ -1,13 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeObjectId } from "./compute_object_id"; import { computeObjectId } from "./compute_object_id";
export const computeStateNameFromEntityAttributes = (
entityId: string,
attributes: { [key: string]: any }
): string =>
attributes.friendly_name === undefined
? computeObjectId(entityId).replace(/_/g, " ")
: attributes.friendly_name || "";
export const computeStateName = (stateObj: HassEntity): string => export const computeStateName = (stateObj: HassEntity): string =>
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@@ -29,8 +29,7 @@ import {
mdiWeatherNight, mdiWeatherNight,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../data/update"; import { updateIsInstalling, UpdateEntity } from "../../data/update";
import { weatherIcon } from "../../data/weather";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
* *
@@ -47,20 +46,6 @@ export const domainIcon = (
stateObj?: HassEntity, stateObj?: HassEntity,
state?: string state?: string
): string => { ): string => {
const icon = domainIconWithoutDefault(domain, stateObj, state);
if (icon) {
return icon;
}
// eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
};
export const domainIconWithoutDefault = (
domain: string,
stateObj?: HassEntity,
state?: string
): string | undefined => {
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
switch (domain) { switch (domain) {
@@ -102,15 +87,6 @@ export const domainIconWithoutDefault = (
? mdiCheckCircleOutline ? mdiCheckCircleOutline
: mdiCloseCircleOutline; : mdiCloseCircleOutline;
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "lock": case "lock":
switch (compareState) { switch (compareState) {
case "unlocked": case "unlocked":
@@ -148,6 +124,15 @@ export const domainIconWithoutDefault = (
break; break;
} }
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "sun": case "sun":
return stateObj?.state === "above_horizon" return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain] ? FIXED_DOMAIN_ICONS[domain]
@@ -159,14 +144,13 @@ export const domainIconWithoutDefault = (
? mdiPackageDown ? mdiPackageDown
: mdiPackageUp : mdiPackageUp
: mdiPackage; : mdiPackage;
case "weather":
return weatherIcon(stateObj?.state);
} }
if (domain in FIXED_DOMAIN_ICONS) { if (domain in FIXED_DOMAIN_ICONS) {
return FIXED_DOMAIN_ICONS[domain]; return FIXED_DOMAIN_ICONS[domain];
} }
return undefined; // eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -3,13 +3,6 @@ import { HassEntity } from "home-assistant-js-websocket";
export const supportsFeature = ( export const supportsFeature = (
stateObj: HassEntity, stateObj: HassEntity,
feature: number feature: number
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
export const supportsFeatureFromAttributes = (
attributes: {
[key: string]: any;
},
feature: number
): boolean => ): boolean =>
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
(attributes.supported_features! & feature) !== 0; (stateObj.attributes.supported_features! & feature) !== 0;

View File

@@ -7,11 +7,8 @@ import { round } from "./round";
* @param stateObj The entity state object * @param stateObj The entity state object
*/ */
export const isNumericState = (stateObj: HassEntity): boolean => export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); !!stateObj.attributes.unit_of_measurement ||
!!stateObj.attributes.state_class;
export const isNumericFromAttributes = (attributes: {
[key: string]: any;
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData

View File

@@ -1,244 +0,0 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}

View File

@@ -1,551 +0,0 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
table.push(row.slice(0));
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number,
fillMinWordPosArr = false
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Diag = 1,
Left = 2,
LeftLeft = 3,
}
/**
* An array representing a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<number>;
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen,
true
)
) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(
patternLen,
wordLen,
patternStart,
wordStart,
patternLow,
wordLow
);
let row: number;
let column = 1;
let patternPos: number;
let wordPos: number;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos =
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
for (
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < nextMaxWordMatchPos;
column++, wordPos++
) {
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos,
wordLen,
wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
: 0; // penalty for a gap start
const canComeLeftLeft =
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (
canComeLeftLeft &&
(!canComeLeft || leftLeftScore >= leftScore) &&
(!canComeDiag || leftLeftScore >= diagScore)
) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
throw new Error(`not possible`);
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
row--;
column--;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn -= 2;
} else if (arrow === Arrow.Left) {
diagColumn -= 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number,
wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
score = 5;
} else if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
score = 5;
isGapLocation = true;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation =
isUpperCaseAtPos(wordPos, word, wordLow) ||
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) {
// first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _arrows = <Arrow[][]>initTable();
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
function _fillInMaxWordMatchPos(
patternLen: number,
wordLen: number,
patternStart: number,
wordStart: number,
patternLow: string,
wordLow: string
) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
export interface FuzzyScorer {
(
pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
}
/**
* A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
*/
export function isEmojiImprecise(x: number): boolean {
return (
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
x === 8986 ||
x === 8987 ||
x === 9200 ||
x === 9203 ||
(x >= 9728 && x <= 10175) ||
x === 11088 ||
x === 11093 ||
(x >= 127744 && x <= 128591) ||
(x >= 128640 && x <= 128764) ||
(x >= 128992 && x <= 129003) ||
(x >= 129280 && x <= 129535) ||
(x >= 129648 && x <= 129750)
);
}

View File

@@ -1,52 +1,4 @@
import { fuzzyScore } from "./filter"; import fuzzysort from "fuzzysort";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {ScorableTextItem} item - Item against whose strings will be checked
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
for (const word of item.strings) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter returns a 0 for a weak match.
// But if .filter() sees a "0", it considers that a failed match and will remove it.
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] === 0 ? 1 : scores[0];
if (score > topScore) {
topScore = score;
}
}
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore;
};
/** /**
* An interface that objects must extend in order to use the fuzzy sequence matcher * An interface that objects must extend in order to use the fuzzy sequence matcher
@@ -66,18 +18,48 @@ export interface ScorableTextItem {
strings: string[]; strings: string[];
} }
type FuzzyFilterSort = <T extends ScorableTextItem>( export type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string, filter: string,
items: T[] items: T[]
) => T[]; ) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => export function fuzzyMatcher(search: string | null): (string) => boolean {
items const scorer = fuzzyScorer(search);
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
}
export function fuzzyScorer(
search: string | null
): (values: string[]) => number {
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
if (!searchTerms) {
return () => 0;
}
return (values) =>
searchTerms
.map((term) => {
const resultsForTerm = fuzzysort.go(term, values, {
allowTypo: true,
});
if (resultsForTerm.length > 0) {
return Math.max(...resultsForTerm.map((result) => result.score));
}
return Number.NEGATIVE_INFINITY;
})
.reduce((partial, current) => partial + current, 0);
}
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
const scorer = fuzzyScorer(filter);
return items
.map((item) => { .map((item) => {
item.score = fuzzySequentialMatch(filter, item); item.score = scorer(item.strings);
return item; return item;
}) })
.filter((item) => item.score !== undefined) .filter((item) => item.score !== undefined && item.score > -100000)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
); );
};
export const defaultFuzzyFilterSort = fuzzySortFilterSort;

View File

@@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
) => { ) => {
let timeout: number | undefined; let timeout: number | undefined;
let previous = 0; let previous = 0;
const throttledFunc = (...args: T): void => { return (...args: T): void => {
const later = () => { const later = () => {
previous = leading === false ? 0 : Date.now(); previous = leading === false ? 0 : Date.now();
timeout = undefined; timeout = undefined;
@@ -35,10 +35,4 @@ export const throttle = <T extends any[]>(
timeout = window.setTimeout(later, remaining); timeout = window.setTimeout(later, remaining);
} }
}; };
throttledFunc.cancel = () => {
clearTimeout(timeout);
timeout = undefined;
previous = 0;
};
return throttledFunc;
}; };

View File

@@ -34,7 +34,7 @@ import {
endOfMonth, endOfMonth,
endOfQuarter, endOfQuarter,
endOfYear, endOfYear,
} from "date-fns/esm"; } from "date-fns";
import { import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,

View File

@@ -269,8 +269,8 @@ export class HaDataTable extends LitElement {
@change=${this._handleHeaderRowCheckboxClick} @change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length && .indeterminate=${this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount} this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length && .checked=${this._checkedRows.length ===
this._checkedRows.length === this._checkableRowsCount} this._checkableRowsCount}
> >
</ha-checkbox> </ha-checkbox>
</div> </div>

View File

@@ -7,25 +7,26 @@ import type {
SortableColumnContainer, SortableColumnContainer,
SortingDirection, SortingDirection,
} from "./ha-data-table"; } from "./ha-data-table";
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
const filterData = ( const filterData = (
data: DataTableRowData[], data: DataTableRowData[],
columns: SortableColumnContainer, columns: SortableColumnContainer,
filter: string filter: string
) => { ) => {
filter = filter.toUpperCase(); const matcher = fuzzyMatcher(filter);
return data.filter((row) => return data.filter((row) =>
Object.entries(columns).some((columnEntry) => { Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
if ( if (
String( matcher(
column.filterKey String(
? row[column.valueColumn || key][column.filterKey] column.filterKey
: row[column.valueColumn || key] ? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
)
) )
.toUpperCase()
.includes(filter)
) { ) {
return true; return true;
} }

View File

@@ -15,12 +15,13 @@ import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface HassEntityWithCachedName extends HassEntity { interface HassEntityWithCachedName extends HassEntity {
friendly_name: string; friendly_name: string;
} }
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
@@ -31,7 +32,6 @@ const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
<span>${item.friendly_name}</span> <span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span> <span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`; </mwc-list-item>`;
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -337,11 +337,18 @@ export class HaEntityPicker extends LitElement {
} }
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value;
(this.comboBox as any).filteredItems = this._states.filter(
(entityState) => const sortableEntityStates = this._states.map((entityState) => ({
entityState.entity_id.toLowerCase().includes(filterString) || strings: [entityState.entity_id, computeStateName(entityState)],
computeStateName(entityState).toLowerCase().includes(filterString) entityState: entityState,
}));
const sortedEntityStates = defaultFuzzyFilterSort(
filterString,
sortableEntityStates
);
(this.comboBox as any).filteredItems = sortedEntityStates.map(
(sortableItem) => sortableItem.entityState
); );
} }

View File

@@ -1,22 +1,17 @@
import type { Button } from "@material/mwc-button";
import "@material/mwc-menu"; import "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HaIconButton } from "./ha-icon-button";
@customElement("ha-button-menu") @customElement("ha-button-menu")
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START"; @property() public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x: number | null = null; @property({ type: Number }) public x?: number;
@property({ type: Number }) public y: number | null = null; @property({ type: Number }) public y?: number;
@property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public multi = false;
@@ -36,18 +31,10 @@ export class HaButtonMenu extends LitElement {
return this._menu?.selected; return this._menu?.selected;
} }
public override focus() {
if (this._menu?.open) {
this._menu.focusItemAtIndex(0);
} else {
this._triggerButton?.focus();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div @click=${this._handleClick}> <div @click=${this._handleClick}>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot> <slot name="trigger"></slot>
</div> </div>
<mwc-menu <mwc-menu
.corner=${this.corner} .corner=${this.corner}
@@ -63,21 +50,6 @@ export class HaButtonMenu extends LitElement {
`; `;
} }
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (document.dir === "rtl") {
this.updateComplete.then(() => {
this.querySelectorAll("mwc-list-item").forEach((item) => {
const style = document.createElement("style");
style.innerHTML =
"span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}";
item!.shadowRoot!.appendChild(style);
});
});
}
}
private _handleClick(): void { private _handleClick(): void {
if (this.disabled) { if (this.disabled) {
return; return;
@@ -86,18 +58,6 @@ export class HaButtonMenu extends LitElement {
this._menu!.show(); this._menu!.show();
} }
private get _triggerButton() {
return this.querySelector(
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
) as HaIconButton | Button | null;
}
private _setTriggerAria() {
if (this._triggerButton) {
this._triggerButton.ariaHasPopup = "menu";
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@@ -66,13 +66,9 @@ export class HaChip extends LitElement {
line-height: 14px; line-height: 14px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
} }
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
.mdc-chip.no-text .mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px; margin-right: -4px;
margin-inline-start: -4px;
margin-inline-end: 4px;
direction: var(--direction);
} }
span[role="gridcell"] { span[role="gridcell"] {

View File

@@ -47,6 +47,10 @@ export class HaClickableListItem extends ListItemBase {
padding-left: 0px; padding-left: 0px;
padding-right: 0px; padding-right: 0px;
} }
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])), :host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) { :host([graphic="icon"]:not([twoLine])) {
height: 48px; height: 48px;
@@ -60,19 +64,6 @@ export class HaClickableListItem extends ListItemBase {
padding-right: var(--mdc-list-side-padding, 20px); padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden; overflow: hidden;
} }
span.material-icons:first-of-type {
margin-inline-start: 0px !important;
margin-inline-end: var(
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
direction: var(--direction);
}
`, `,
]; ];
} }

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import { UNAVAILABLE_STATES } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@customElement("ha-climate-state") @customElement("ha-climate-state")
@@ -16,22 +15,22 @@ class HaClimateState extends LitElement {
const currentStatus = this._computeCurrentStatus(); const currentStatus = this._computeCurrentStatus();
return html`<div class="target"> return html`<div class="target">
${!UNAVAILABLE_STATES.includes(this.stateObj.state) ${this.stateObj.state !== "unknown"
? html`<span class="state-label"> ? html`<span class="state-label">
${this._localizeState()} ${this._localizeState()}
${this.stateObj.attributes.preset_mode && ${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`- ? html`-
${this.hass.localize( ${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}` ) || this.stateObj.attributes.preset_mode}`
: ""} : ""}
</span> </span>`
<div class="unit">${this._computeTarget()}</div>` : ""}
: this._localizeState()} <div class="unit">${this._computeTarget()}</div>
</div> </div>
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state) ${currentStatus
? html`<div class="current"> ? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}: ${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div> <div class="unit">${currentStatus}</div>
@@ -109,10 +108,6 @@ class HaClimateState extends LitElement {
} }
private _localizeState(): string { private _localizeState(): string {
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.localize( const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}` `component.climate.state._.${this.stateObj.state}`
); );

View File

@@ -1,17 +1,13 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type { import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light";
ComboBoxLight,
ComboBoxLightFilterChangedEvent,
ComboBoxLightOpenedChangedEvent,
ComboBoxLightValueChangedEvent,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield"; import "./ha-textfield";
@@ -100,8 +96,6 @@ export class HaComboBox extends LitElement {
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; @query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
private _overlayMutationObserver?: MutationObserver;
public open() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this._comboBox?.open(); this._comboBox?.open();
@@ -114,14 +108,6 @@ export class HaComboBox extends LitElement {
}); });
} }
public disconnectedCallback() {
super.disconnectedCallback();
if (this._overlayMutationObserver) {
this._overlayMutationObserver.disconnect();
this._overlayMutationObserver = undefined;
}
}
public get selectedItem() { public get selectedItem() {
return this._comboBox.selectedItem; return this._comboBox.selectedItem;
} }
@@ -207,64 +193,21 @@ export class HaComboBox extends LitElement {
} }
} }
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
const opened = ev.detail.value;
// delay this so we can handle click event before setting _opened // delay this so we can handle click event before setting _opened
setTimeout(() => { setTimeout(() => {
this._opened = opened; this._opened = ev.detail.value;
}, 0); }, 0);
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail); fireEvent(this, ev.type, ev.detail);
if (
opened &&
"MutationObserver" in window &&
!this._overlayMutationObserver
) {
const overlay = document.querySelector<HTMLElement>(
"vaadin-combo-box-overlay"
);
if (!overlay) {
return;
}
this._overlayMutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "inert"
) {
this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined;
// @ts-expect-error
overlay.inert = false;
} else if (mutation.type === "childList") {
mutation.removedNodes.forEach((node) => {
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined;
}
});
}
});
});
this._overlayMutationObserver.observe(overlay, {
attributes: true,
});
this._overlayMutationObserver.observe(document.body, {
childList: true,
});
}
} }
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) { private _filterChanged(ev: PolymerChangedEvent<string>) {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail, { composed: false }); fireEvent(this, ev.type, ev.detail, { composed: false });
} }
private _valueChanged(ev: ComboBoxLightValueChangedEvent) { private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;
@@ -298,9 +241,6 @@ export class HaComboBox extends LitElement {
.toggle-button { .toggle-button {
right: 12px; right: 12px;
top: -10px; top: -10px;
inset-inline-start: initial;
inset-inline-end: 12px;
direction: var(--direction);
} }
:host([opened]) .toggle-button { :host([opened]) .toggle-button {
color: var(--primary-color); color: var(--primary-color);
@@ -309,9 +249,18 @@ export class HaComboBox extends LitElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
top: -7px; top: -7px;
right: 36px; right: 36px;
inset-inline-start: initial; }
inset-inline-end: 36px;
direction: var(--direction); :host-context([style*="direction: rtl;"]) .toggle-button {
left: 12px;
right: auto;
top: -10px;
}
:host-context([style*="direction: rtl;"]) .clear-button {
--mdc-icon-size: 20px;
top: -7px;
left: 36px;
right: auto;
} }
`; `;
} }

View File

@@ -140,9 +140,6 @@ export class HaDateRangePicker extends LitElement {
return css` return css`
ha-svg-icon { ha-svg-icon {
margin-right: 8px; margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
direction: var(--direction);
} }
.date-range-inputs { .date-range-inputs {
@@ -169,9 +166,6 @@ export class HaDateRangePicker extends LitElement {
ha-textfield:last-child { ha-textfield:last-child {
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
direction: var(--direction);
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {

View File

@@ -3,8 +3,8 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit"; import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import "./ha-icon-button"; import "./ha-icon-button";
export const createCloseHeading = ( export const createCloseHeading = (
@@ -17,13 +17,12 @@ export const createCloseHeading = (
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"
dir=${computeRTLDirection(hass)}
></ha-icon-button> ></ha-icon-button>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends DialogBase { export class HaDialog extends DialogBase {
protected readonly [FOCUS_TARGET];
public scrollToPos(x: number, y: number) { public scrollToPos(x: number, y: number) {
this.contentElement?.scrollTo(x, y); this.contentElement?.scrollTo(x, y);
} }
@@ -90,18 +89,18 @@ export class HaDialog extends DialogBase {
} }
.header_title { .header_title {
margin-right: 40px; margin-right: 40px;
margin-inline-end: 40px;
direction: var(--direction);
} }
.header_button { [dir="rtl"].header_button {
inset-inline-start: initial; right: auto;
inset-inline-end: 16px; left: 16px;
direction: var(--direction);
} }
.dialog-actions { [dir="rtl"].header_title {
inset-inline-start: initial !important; margin-left: 40px;
inset-inline-end: 0px !important; margin-right: 0px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) .dialog-actions {
left: 0px !important;
right: auto !important;
} }
`, `,
]; ];

View File

@@ -133,9 +133,6 @@ class HaExpansionPanel extends LitElement {
.summary-icon { .summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto; margin-left: auto;
margin-inline-start: auto;
margin-inline-end: initial;
direction: var(--direction);
} }
.summary-icon.expanded { .summary-icon.expanded {

View File

@@ -1,25 +1,24 @@
import { FabBase } from "@material/mwc-fab/mwc-fab-base"; import { Fab } from "@material/mwc-fab";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { css } from "lit"; import { css } from "lit";
@customElement("ha-fab") @customElement("ha-fab")
export class HaFab extends FabBase { export class HaFab extends Fab {
protected firstUpdated(changedProperties) { protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
} }
static override styles = [ static override styles = Fab.styles.concat([
styles,
css` css`
:host .mdc-fab--extended .mdc-fab__icon { :host-context([style*="direction: rtl;"])
margin-inline-start: -8px; .mdc-fab--extended
margin-inline-end: 12px; .mdc-fab__icon {
direction: var(--direction); margin-left: 12px !important;
margin-right: calc(12px - 20px) !important;
} }
`, `,
]; ]);
} }
declare global { declare global {

View File

@@ -175,23 +175,24 @@ export class HaFileUpload extends LitElement {
} }
.mdc-text-field__icon--leading { .mdc-text-field__icon--leading {
margin-bottom: 12px; margin-bottom: 12px;
inset-inline-start: initial; }
inset-inline-end: 0px; :host-context([style*="direction: rtl;"])
direction: var(--direction); .mdc-text-field__icon--leading {
margin-right: 0px;
} }
.mdc-text-field--filled .mdc-floating-label--float-above { .mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75); transform: scale(0.75);
top: 8px; top: 8px;
} }
.mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 16px !important; left: initial;
inset-inline-end: initial !important; right: 16px;
direction: var(--direction);
} }
.mdc-text-field--filled .mdc-floating-label { :host-context([style*="direction: rtl;"])
inset-inline-start: 48px !important; .mdc-text-field--filled
inset-inline-end: initial !important; .mdc-floating-label {
direction: var(--direction); left: initial;
right: 48px;
} }
.dragged:before { .dragged:before {
position: var(--layout-fit_-_position); position: var(--layout-fit_-_position);

View File

@@ -133,10 +133,9 @@ export class HaFormString extends LitElement implements HaFormElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-icon-button { :host-context([style*="direction: rtl;"]) ha-icon-button {
inset-inline-start: initial; right: auto;
inset-inline-end: 12px; left: 12px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -28,15 +28,10 @@ export class HaFormfield extends FormfieldBase {
css` css`
:host(:not([alignEnd])) ::slotted(ha-switch) { :host(:not([alignEnd])) ::slotted(ha-switch) {
margin-right: 10px; margin-right: 10px;
margin-inline-end: 10px;
margin-inline-start: inline;
} }
.mdc-form-field > label { :host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) {
direction: var(--direction); margin-left: 10px;
margin-inline-start: 0; margin-right: auto;
margin-inline-end: auto;
padding-inline-start: 4px;
padding-inline-end: 0;
} }
`, `,
]; ];

View File

@@ -1,8 +1,6 @@
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import type { IconButton } from "@material/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-icon-button") @customElement("ha-icon-button")
@@ -13,32 +11,21 @@ export class HaIconButton extends LitElement {
@property({ type: String }) path?: string; @property({ type: String }) path?: string;
// Label that is used for ARIA support and as tooltip // Label that is used for ARIA support and as tooltip
@property({ type: String }) label?: string; @property({ type: String }) label = "";
// These should always be set as properties, not attributes,
// so that only the <button> element gets the attribute
@property({ type: String, attribute: "aria-haspopup" })
override ariaHasPopup!: IconButton["ariaHasPopup"];
@property({ type: Boolean }) hideTitle = false; @property({ type: Boolean }) hideTitle = false;
@query("mwc-icon-button", true) private _button?: IconButton;
public override focus() {
this._button?.focus();
}
static shadowRootOptions: ShadowRootInit = { static shadowRootOptions: ShadowRootInit = {
mode: "open", mode: "open",
delegatesFocus: true, delegatesFocus: true,
}; };
protected render(): TemplateResult { protected render(): TemplateResult {
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
return html` return html`
<mwc-icon-button <mwc-icon-button
aria-label=${ifDefined(this.label)} .ariaLabel=${this.label}
title=${ifDefined(this.hideTitle ? undefined : this.label)} .title=${this.hideTitle ? "" : this.label}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
.disabled=${this.disabled} .disabled=${this.disabled}
> >
${this.path ${this.path

View File

@@ -47,18 +47,9 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor { .mdc-select__anchor {
width: var(--ha-select-min-width, 200px); width: var(--ha-select-min-width, 200px);
} }
.mdc-select--filled .mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 12px; right: 16px !important;
inset-inline-end: initial; left: initial !important;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: 12px;
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
} }
`, `,
]; ];

View File

@@ -1,24 +1,15 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry"; import { DeviceRegistryEntry } from "../../data/device_registry";
import { import { EntityRegistryEntry } from "../../data/entity_registry";
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { AreaSelector } from "../../data/selector"; import { AreaSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-area-picker"; import "../ha-area-picker";
import "../ha-areas-picker"; import "../ha-areas-picker";
@customElement("ha-selector-area") @customElement("ha-selector-area")
export class HaAreaSelector extends SubscribeMixin(LitElement) { export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector; @property() public selector!: AreaSelector;
@@ -29,44 +20,29 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string; @property() public helper?: string;
@state() private _entitySources?: EntitySources; @state() public _configEntries?: ConfigEntry[];
@state() private _entities?: EntityRegistryEntry[];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties) { protected updated(changedProperties) {
if ( if (changedProperties.has("selector")) {
changedProperties.has("selector") && const oldSelector = changedProperties.get("selector");
(this.selector.area.device?.integration || if (
this.selector.area.entity?.integration) && oldSelector !== this.selector &&
!this._entitySources this.selector.area.device?.integration
) { ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => { getConfigEntries(this.hass, {
this._entitySources = sources; domain: this.selector.area.device.integration,
}); }).then((entries) => {
this._configEntries = entries;
});
}
} }
} }
protected render() { protected render() {
if (
(this.selector.area.device?.integration ||
this.selector.area.entity?.integration) &&
!this._entitySources
) {
return html``;
}
if (!this.selector.area.multiple) { if (!this.selector.area.multiple) {
return html` return html`
<ha-area-picker <ha-area-picker
@@ -111,62 +87,39 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
} }
private _filterEntities = (entity: EntityRegistryEntry): boolean => { private _filterEntities = (entity: EntityRegistryEntry): boolean => {
const filterIntegration = this.selector.area.entity?.integration; if (this.selector.area.entity?.integration) {
if ( if (entity.platform !== this.selector.area.entity.integration) {
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.area.device) {
return true;
}
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = this.selector.area.device;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false;
}
if (filterModel && device.model !== filterModel) {
return false;
}
if (filterIntegration && this._entitySources && this._entities) {
const deviceIntegrations = this._deviceIntegrations(
this._entitySources,
this._entities
);
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
return false; return false;
} }
} }
return true; return true;
}; };
private _deviceIntegrations = memoizeOne( private _filterDevices = (device: DeviceRegistryEntry): boolean => {
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => { if (
const deviceIntegrations: Record<string, string[]> = {}; this.selector.area.device?.manufacturer &&
device.manufacturer !== this.selector.area.device.manufacturer
for (const entity of entities) { ) {
const source = entitySources[entity.entity_id]; return false;
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
} }
); if (
this.selector.area.device?.model &&
device.model !== this.selector.area.device.model
) {
return false;
}
if (this.selector.area.device?.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
};
} }
declare global { declare global {

View File

@@ -1,33 +1,18 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { ConfigEntry } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector"; import type { DeviceSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker"; import "../device/ha-device-picker";
import "../device/ha-devices-picker"; import "../device/ha-devices-picker";
@customElement("ha-selector-device") @customElement("ha-selector-device")
export class HaDeviceSelector extends SubscribeMixin(LitElement) { export class HaDeviceSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector; @property() public selector!: DeviceSelector;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
@property() public value?: any; @property() public value?: any;
@property() public label?: string; @property() public label?: string;
@@ -40,32 +25,20 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
public hassSubscribe(): UnsubscribeFunc[] { protected updated(changedProperties) {
return [ if (changedProperties.has("selector")) {
subscribeEntityRegistry(this.hass.connection!, (entities) => { const oldSelector = changedProperties.get("selector");
this._entities = entities.filter((entity) => entity.device_id !== null); if (oldSelector !== this.selector && this.selector.device?.integration) {
}), getConfigEntries(this.hass, {
]; domain: this.selector.device.integration,
} }).then((entries) => {
this._configEntries = entries;
protected updated(changedProperties): void { });
super.updated(changedProperties); }
if (
changedProperties.has("selector") &&
this.selector.device.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
} }
} }
protected render() { protected render() {
if (this.selector.device.integration && !this._entitySources) {
return html``;
}
if (!this.selector.device.multiple) { if (!this.selector.device.multiple) {
return html` return html`
<ha-device-picker <ha-device-picker
@@ -107,48 +80,30 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
} }
private _filterDevices = (device: DeviceRegistryEntry): boolean => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
const { if (
manufacturer: filterManufacturer, this.selector.device?.manufacturer &&
model: filterModel, device.manufacturer !== this.selector.device.manufacturer
integration: filterIntegration, ) {
} = this.selector.device;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false; return false;
} }
if (filterModel && device.model !== filterModel) { if (
this.selector.device?.model &&
device.model !== this.selector.device.model
) {
return false; return false;
} }
if (filterIntegration && this._entitySources && this._entities) { if (this.selector.device?.integration) {
const deviceIntegrations = this._deviceIntegrations( if (
this._entitySources, this._configEntries &&
this._entities !this._configEntries.some((entry) =>
); device.config_entries.includes(entry.entry_id)
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { )
) {
return false; return false;
} }
} }
return true; return true;
}; };
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
} }
declare global { declare global {

View File

@@ -1,3 +1,4 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
@@ -46,14 +47,14 @@ export class HaSelectSelector extends LitElement {
${this.label} ${this.label}
${options.map( ${options.map(
(item: SelectOption) => html` (item: SelectOption) => html`
<ha-formfield .label=${item.label}> <mwc-formfield .label=${item.label}>
<ha-radio <ha-radio
.checked=${item.value === this.value} .checked=${item.value === this.value}
.value=${item.value} .value=${item.value}
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-radio> ></ha-radio>
</ha-formfield> </mwc-formfield>
` `
)} )}
</div> </div>

View File

@@ -287,7 +287,9 @@ export class HaServiceControl extends LitElement {
${shouldRenderServiceDataYaml ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize("ui.components.service-control.data")} .label=${this.hass.localize(
"ui.components.service-control.service_data"
)}
.name=${"data"} .name=${"data"}
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}

View File

@@ -569,9 +569,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
height: 16px; height: 16px;
--mdc-icon-size: 14px; --mdc-icon-size: 14px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
margin-inline-start: 4px !important;
margin-inline-end: -4px !important;
direction: var(--direction);
} }
.mdc-chip__icon--leading { .mdc-chip__icon--leading {
display: flex; display: flex;
@@ -581,9 +578,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
border-radius: 50%; border-radius: 50%;
padding: 6px; padding: 6px;
margin-left: -14px !important; margin-left: -14px !important;
margin-inline-start: -14px !important;
margin-inline-end: 4px !important;
direction: var(--direction);
} }
.expand-btn { .expand-btn {
margin-right: 0; margin-right: 0;
@@ -622,6 +616,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
opacity: var(--light-disabled-opacity); opacity: var(--light-disabled-opacity);
pointer-events: none; pointer-events: none;
} }
:host-context([style*="direction: rtl;"]) .mdc-chip__icon {
margin-right: -14px !important;
margin-left: 4px !important;
}
`; `;
} }
} }

View File

@@ -57,9 +57,6 @@ export class HaTextField extends TextFieldBase {
.mdc-text-field__affix--suffix { .mdc-text-field__affix--suffix {
padding-left: var(--text-field-suffix-padding-left, 12px); padding-left: var(--text-field-suffix-padding-left, 12px);
padding-right: var(--text-field-suffix-padding-right, 0px); padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
direction: var(--direction);
} }
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field:not(.mdc-text-field--disabled)
@@ -95,19 +92,17 @@ export class HaTextField extends TextFieldBase {
overflow: var(--text-field-overflow); overflow: var(--text-field-overflow);
} }
.mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 16px !important; right: 10px !important;
inset-inline-end: initial !important; left: initial !important;
transform-origin: var(--float-start);
direction: var(--direction);
} }
.mdc-text-field--with-leading-icon.mdc-text-field--filled :host-context([style*="direction: rtl;"])
.mdc-text-field--with-leading-icon.mdc-text-field--filled
.mdc-floating-label { .mdc-floating-label {
max-width: calc(100% - 48px); max-width: calc(100% - 48px);
inset-inline-start: 48px !important; right: 48px !important;
inset-inline-end: initial !important; left: initial !important;
direction: var(--direction);
} }
`, `,
]; ];

View File

@@ -314,10 +314,9 @@ class DialogMediaManage extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px !important; margin-left: 8px !important;
margin-inline-end: 8px !important; margin-right: 0px !important;
direction: var(--direction);
} }
.refresh { .refresh {

View File

@@ -60,10 +60,9 @@ class MediaManageButton extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px; margin-left: 8px;
margin-inline-end: 8px; margin-right: 0px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -1,11 +1,10 @@
import "@lit-labs/virtualizer";
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import { grid } from "@lit-labs/virtualizer/layouts/grid";
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { grid } from "@lit-labs/virtualizer/layouts/grid";
import "@lit-labs/virtualizer";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -22,13 +21,10 @@ import {
state, state,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl"; import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { getSignedPath } from "../../data/auth";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { MediaPlayerItem } from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player";
import { import {
browseMediaPlayer, browseMediaPlayer,
@@ -43,10 +39,8 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
import "../ha-alert";
import "../ha-button-menu"; import "../ha-button-menu";
import "../ha-card"; import "../ha-card";
import "../ha-circular-progress"; import "../ha-circular-progress";
@@ -55,6 +49,8 @@ import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-browse-media-tts"; import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { getSignedPath } from "../../data/auth";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -104,10 +100,6 @@ export class HaMediaPlayerBrowse extends LitElement {
@query(".content") private _content?: HTMLDivElement; @query(".content") private _content?: HTMLDivElement;
@query("lit-virtualizer") private _virtualizer?: LitVirtualizer;
private _observed = false;
private _headerOffsetHeight = 0; private _headerOffsetHeight = 0;
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
@@ -248,16 +240,6 @@ export class HaMediaPlayerBrowse extends LitElement {
], ],
replace: true, replace: true,
}); });
} else if (
err.code === "entity_not_found" &&
UNAVAILABLE_STATES.includes(this.hass.states[this.entityId]?.state)
) {
this._setError({
message: this.hass.localize(
`ui.components.media-browser.media_player_unavailable`
),
code: "entity_not_found",
});
} else { } else {
this._setError(err); this._setError(err);
} }
@@ -298,30 +280,13 @@ export class HaMediaPlayerBrowse extends LitElement {
this._animateHeaderHeight(); this._animateHeaderHeight();
} else if (changedProps.has("_currentItem")) { } else if (changedProps.has("_currentItem")) {
this._setHeaderHeight(); this._setHeaderHeight();
// This fixes a race condition for resizing of the cards using the grid layout
if (this._observed) {
return;
}
// @ts-ignore
const virtualizer = this._virtualizer?._virtualizer;
if (virtualizer) {
this._observed = true;
setTimeout(() => virtualizer._observeMutations(), 0);
}
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._error) { if (this._error) {
return html` return html`
<div class="container"> <div class="container">${this._renderError(this._error)}</div>
<ha-alert alert-type="error">
${this._renderError(this._error)}
</ha-alert>
</div>
`; `;
} }
@@ -436,9 +401,7 @@ export class HaMediaPlayerBrowse extends LitElement {
this._error this._error
? html` ? html`
<div class="container"> <div class="container">
<ha-alert alert-type="error"> ${this._renderError(this._error)}
${this._renderError(this._error)}
</ha-alert>
</div> </div>
` `
: isTTSMediaSource(currentItem.media_content_id) : isTTSMediaSource(currentItem.media_content_id)
@@ -514,9 +477,6 @@ export class HaMediaPlayerBrowse extends LitElement {
<lit-virtualizer <lit-virtualizer
scroller scroller
.items=${children} .items=${children}
style=${styleMap({
height: `${children.length * 72 + 26}px`,
})}
.renderItem=${this._renderListItem} .renderItem=${this._renderListItem}
></lit-virtualizer> ></lit-virtualizer>
${currentItem.not_shown ${currentItem.not_shown
@@ -646,6 +606,7 @@ export class HaMediaPlayerBrowse extends LitElement {
</div> </div>
<span class="title">${child.title}</span> <span class="title">${child.title}</span>
</mwc-list-item> </mwc-list-item>
<li divider role="separator"></li>
`; `;
}; };

View File

@@ -120,10 +120,9 @@ class MediaUploadButton extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
ha-svg-icon[slot="icon"] { :host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-inline-start: 0px; margin-left: 8px;
margin-inline-end: 8px; margin-right: 0px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -10,8 +10,6 @@ export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public raised = false; @property({ type: Boolean, reflect: true }) public raised = false;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ type: Boolean }) public lastItem = false; @property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string; @property({ type: String }) public icon?: string;
@@ -78,9 +76,6 @@ export class HaTimeline extends LitElement {
margin-right: 8px; margin-right: 8px;
width: 24px; width: 24px;
} }
:host([notEnabled]) ha-svg-icon {
opacity: 0.5;
}
ha-svg-icon { ha-svg-icon {
color: var( color: var(
--timeline-ball-color, --timeline-ball-color,

View File

@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./hat-logbook-note"; import "./hat-logbook-note";
import "../../panels/logbook/ha-logbook-renderer"; import "../../panels/logbook/ha-logbook";
import { TraceExtended } from "../../data/trace"; import { TraceExtended } from "../../data/trace";
@customElement("ha-trace-logbook") @customElement("ha-trace-logbook")
@@ -19,12 +19,12 @@ export class HaTraceLogbook extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return this.logbookEntries.length return this.logbookEntries.length
? html` ? html`
<ha-logbook-renderer <ha-logbook
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${this.logbookEntries} .entries=${this.logbookEntries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">

View File

@@ -13,7 +13,7 @@ import {
getDataFromPath, getDataFromPath,
TraceExtended, TraceExtended,
} from "../../data/trace"; } from "../../data/trace";
import "../../panels/logbook/ha-logbook-renderer"; import "../../panels/logbook/ha-logbook";
import { traceTabStyles } from "./trace-tab-styles"; import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph"; import type { NodeInfo } from "./hat-script-graph";
@@ -114,11 +114,6 @@ export class HaTracePathDetails extends LitElement {
const { path, timestamp, result, error, changed_variables, ...rest } = const { path, timestamp, result, error, changed_variables, ...rest } =
trace as any; trace as any;
if (result?.enabled === false) {
return html`This node was disabled and skipped during execution so
no further trace information is available.`;
}
return html` return html`
${curPath === this.selected.path ${curPath === this.selected.path
? "" ? ""
@@ -194,7 +189,7 @@ export class HaTracePathDetails extends LitElement {
// it's the last entry. Find all logbook entries after start. // it's the last entry. Find all logbook entries after start.
const startTime = new Date(startTrace[0].timestamp); const startTime = new Date(startTrace[0].timestamp);
const idx = this.logbookEntries.findIndex( const idx = this.logbookEntries.findIndex(
(entry) => new Date(entry.when * 1000) >= startTime (entry) => new Date(entry.when) >= startTime
); );
if (idx === -1) { if (idx === -1) {
entries = []; entries = [];
@@ -210,7 +205,7 @@ export class HaTracePathDetails extends LitElement {
entries = []; entries = [];
for (const entry of this.logbookEntries || []) { for (const entry of this.logbookEntries || []) {
const entryDate = new Date(entry.when * 1000); const entryDate = new Date(entry.when);
if (entryDate >= startTime) { if (entryDate >= startTime) {
if (entryDate < endTime) { if (entryDate < endTime) {
entries.push(entry); entries.push(entry);
@@ -224,12 +219,12 @@ export class HaTracePathDetails extends LitElement {
return entries.length return entries.length
? html` ? html`
<ha-logbook-renderer <ha-logbook
relative-time relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${entries} .entries=${entries}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-logbook-renderer> ></ha-logbook>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">

View File

@@ -116,14 +116,9 @@ export class HatGraphNode extends LitElement {
--stroke-clr: var(--hover-clr); --stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr); --icon-clr: var(--default-icon-clr);
} }
:host([notEnabled]) circle { :host([notEnabled]) circle,
--stroke-clr: var(--disabled-clr); :host([notEnabled]) path.connector {
} stroke: var(--disabled-clr);
:host([notEnabled][active]) circle {
--stroke-clr: var(--disabled-active-clr);
}
:host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr);
} }
svg { svg {
width: 100%; width: 100%;

View File

@@ -754,8 +754,6 @@ export class HatScriptGraph extends LitElement {
--track-clr: var(--track-color, var(--accent-color)); --track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-color)); --hover-clr: var(--hover-color, var(--primary-color));
--disabled-clr: var(--disabled-color, var(--disabled-text-color)); --disabled-clr: var(--disabled-color, var(--disabled-text-color));
--disabled-active-clr: rgba(var(--rgb-primary-color), 0.5);
--disabled-hover-clr: rgba(var(--rgb-primary-color), 0.7);
--default-trigger-color: 3, 169, 244; --default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color)); --rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white); --background-clr: var(--background-color, white);

View File

@@ -116,7 +116,7 @@ class LogbookRenderer {
maybeRenderItem() { maybeRenderItem() {
const logbookEntry = this.curItem; const logbookEntry = this.curItem;
this.curIndex++; this.curIndex++;
const entryDate = new Date(logbookEntry.when * 1000); const entryDate = new Date(logbookEntry.when);
if (this.pendingItems.length === 0) { if (this.pendingItems.length === 0) {
this.pendingItems.push([entryDate, logbookEntry]); this.pendingItems.push([entryDate, logbookEntry]);
@@ -248,7 +248,7 @@ class ActionRenderer {
// Render all logbook items that are in front of this item. // Render all logbook items that are in front of this item.
while ( while (
this.logbookRenderer.hasNext && this.logbookRenderer.hasNext &&
new Date(this.logbookRenderer.curItem.when * 1000) < timestamp new Date(this.logbookRenderer.curItem.when) < timestamp
) { ) {
this.logbookRenderer.maybeRenderItem(); this.logbookRenderer.maybeRenderItem();
} }
@@ -296,12 +296,7 @@ class ActionRenderer {
return this._handleParallel(index); return this._handleParallel(index);
} }
this._renderEntry( this._renderEntry(path, describeAction(this.hass, data, actionType));
path,
describeAction(this.hass, data, actionType),
undefined,
data.enabled === false
);
let i = index + 1; let i = index + 1;
@@ -354,16 +349,10 @@ class ActionRenderer {
const chooseConfig = this._getDataFromPath( const chooseConfig = this._getDataFromPath(
this.keys[index] this.keys[index]
) as ChooseAction; ) as ChooseAction;
const disabled = chooseConfig.enabled === false;
const name = chooseConfig.alias || "Choose"; const name = chooseConfig.alias || "Choose";
if (defaultExecuted) { if (defaultExecuted) {
this._renderEntry( this._renderEntry(choosePath, `${name}: Default action executed`);
choosePath,
`${name}: Default action executed`,
undefined,
disabled
);
} else if (chooseTrace.result) { } else if (chooseTrace.result) {
const choiceNumeric = const choiceNumeric =
chooseTrace.result.choice !== "default" chooseTrace.result.choice !== "default"
@@ -375,19 +364,9 @@ class ActionRenderer {
const choiceName = choiceConfig const choiceName = choiceConfig
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed` ? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
: `Error: ${chooseTrace.error}`; : `Error: ${chooseTrace.error}`;
this._renderEntry( this._renderEntry(choosePath, `${name}: ${choiceName}`);
choosePath,
`${name}: ${choiceName}`,
undefined,
disabled
);
} else { } else {
this._renderEntry( this._renderEntry(choosePath, `${name}: No action taken`);
choosePath,
`${name}: No action taken`,
undefined,
disabled
);
} }
let i; let i;
@@ -435,11 +414,9 @@ class ActionRenderer {
const repeatConfig = this._getDataFromPath( const repeatConfig = this._getDataFromPath(
this.keys[index] this.keys[index]
) as RepeatAction; ) as RepeatAction;
const disabled = repeatConfig.enabled === false;
const name = repeatConfig.alias || describeAction(this.hass, repeatConfig); const name = repeatConfig.alias || describeAction(this.hass, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled); this._renderEntry(repeatPath, name);
let i; let i;
@@ -464,24 +441,18 @@ class ActionRenderer {
const ifTrace = this._getItem(index)[0] as IfActionTraceStep; const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction; const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
const disabled = ifConfig.enabled === false;
const name = ifConfig.alias || "If"; const name = ifConfig.alias || "If";
if (ifTrace.result?.choice) { if (ifTrace.result) {
const choiceConfig = this._getDataFromPath( const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/${ifTrace.result.choice}/` `${this.keys[index]}/${ifTrace.result.choice}/`
) as any; ) as any;
const choiceName = choiceConfig const choiceName = choiceConfig
? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}` ? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}`
: `Error: ${ifTrace.error}`; : `Error: ${ifTrace.error}`;
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled); this._renderEntry(ifPath, `${name}: ${choiceName}`);
} else { } else {
this._renderEntry( this._renderEntry(ifPath, `${name}: No action taken`);
ifPath,
`${name}: No action taken`,
undefined,
disabled
);
} }
let i; let i;
@@ -518,11 +489,9 @@ class ActionRenderer {
this.keys[index] this.keys[index]
) as ParallelAction; ) as ParallelAction;
const disabled = parallelConfig.enabled === false;
const name = parallelConfig.alias || "Execute in parallel"; const name = parallelConfig.alias || "Execute in parallel";
this._renderEntry(parallelPath, name, undefined, disabled); this._renderEntry(parallelPath, name);
let i; let i;
@@ -544,14 +513,11 @@ class ActionRenderer {
private _renderEntry( private _renderEntry(
path: string, path: string,
description: string, description: string,
icon = mdiRecordCircleOutline, icon = mdiRecordCircleOutline
disabled = false
) { ) {
this.entries.push(html` this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}> <ha-timeline .icon=${icon} data-path=${path}>
${description}${disabled ${description}
? html`<span class="disabled"> (disabled)</span>`
: ""}
</ha-timeline> </ha-timeline>
`); `);
} }

View File

@@ -1,47 +0,0 @@
import { HomeAssistant } from "../types";
export interface ApplicationCredentialsConfig {
domains: string[];
}
export interface ApplicationCredential {
id: string;
domain: string;
client_id: string;
client_secret: string;
name: string;
}
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredentialsConfig>({
type: "application_credentials/config",
});
export const fetchApplicationCredentials = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredential[]>({
type: "application_credentials/list",
});
export const createApplicationCredential = async (
hass: HomeAssistant,
domain: string,
clientId: string,
clientSecret: string,
name?: string
) =>
hass.callWS<ApplicationCredential>({
type: "application_credentials/create",
domain,
client_id: clientId,
client_secret: clientSecret,
name,
});
export const deleteApplicationCredential = async (
hass: HomeAssistant,
applicationCredentialsId: string
) =>
hass.callWS<void>({
type: "application_credentials/delete",
application_credentials_id: applicationCredentialsId,
});

View File

@@ -157,7 +157,6 @@ export interface CalendarTrigger extends BaseTrigger {
platform: "calendar"; platform: "calendar";
event: "start" | "end"; event: "start" | "end";
entity_id: string; entity_id: string;
offset: string;
} }
export type Trigger = export type Trigger =

View File

@@ -1,13 +1,13 @@
import { HassEntity } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { import {
computeHistory, computeHistory,
HistoryStates, fetchRecent,
HistoryResult, HistoryResult,
LineChartUnit, LineChartUnit,
TimelineEntity, TimelineEntity,
entityIdHistoryNeedsAttributes, entityIdHistoryNeedsAttributes,
fetchRecentWS,
} from "./history"; } from "./history";
export interface CacheConfig { export interface CacheConfig {
@@ -34,7 +34,7 @@ const RECENT_THRESHOLD = 60000; // 1 minute
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {}; const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {}; const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cached type 1 function. Without cache config. // Cached type 1 unction. Without cache config.
export const getRecent = ( export const getRecent = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,
@@ -55,7 +55,7 @@ export const getRecent = (
} }
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecentWS( const prom = fetchRecent(
hass, hass,
entityId, entityId,
startTime, startTime,
@@ -103,14 +103,13 @@ export const getRecentWithCache = (
language: string language: string
) => { ) => {
const cacheKey = cacheConfig.cacheKey; const cacheKey = cacheConfig.cacheKey;
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
const endTime = new Date(); const endTime = new Date();
const startTime = new Date(endTime); const startTime = new Date(endTime);
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow); startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
let toFetchStartTime = startTime; let toFetchStartTime = startTime;
let appendingToCache = false; let appendingToCache = false;
let cache = stateHistoryCache[fullCacheKey]; let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
if ( if (
cache && cache &&
toFetchStartTime >= cache.startTime && toFetchStartTime >= cache.startTime &&
@@ -124,7 +123,7 @@ export const getRecentWithCache = (
return cache.prom; return cache.prom;
} }
} else { } else {
cache = stateHistoryCache[fullCacheKey] = getEmptyCache( cache = stateHistoryCache[cacheKey] = getEmptyCache(
language, language,
startTime, startTime,
endTime endTime
@@ -135,12 +134,12 @@ export const getRecentWithCache = (
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => { const genProm = async () => {
let fetchedHistory: HistoryStates; let fetchedHistory: HassEntity[][];
try { try {
const results = await Promise.all([ const results = await Promise.all([
curCacheProm, curCacheProm,
fetchRecentWS( fetchRecent(
hass, hass,
entityId, entityId,
toFetchStartTime, toFetchStartTime,
@@ -153,7 +152,7 @@ export const getRecentWithCache = (
]); ]);
fetchedHistory = results[1]; fetchedHistory = results[1];
} catch (err: any) { } catch (err: any) {
delete stateHistoryCache[fullCacheKey]; delete stateHistoryCache[cacheKey];
throw err; throw err;
} }
const stateHistory = computeHistory(hass, fetchedHistory, localize); const stateHistory = computeHistory(hass, fetchedHistory, localize);

View File

@@ -1,14 +1,11 @@
import { import {
addDays,
addHours, addHours,
addMilliseconds,
addMonths,
differenceInDays, differenceInDays,
endOfToday, endOfToday,
endOfYesterday, endOfYesterday,
startOfToday, startOfToday,
startOfYesterday, startOfYesterday,
} from "date-fns/esm"; } from "date-fns";
import { Collection, getCollection } from "home-assistant-js-websocket"; import { Collection, getCollection } from "home-assistant-js-websocket";
import { groupBy } from "../common/util/group-by"; import { groupBy } from "../common/util/group-by";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
@@ -17,9 +14,9 @@ import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry"; import { subscribeEntityRegistry } from "./entity_registry";
import { import {
fetchStatistics, fetchStatistics,
getStatisticMetadata,
Statistics, Statistics,
StatisticsMetaData, StatisticsMetaData,
getStatisticMetadata,
} from "./history"; } from "./history";
const energyCollectionKeys: (string | undefined)[] = []; const energyCollectionKeys: (string | undefined)[] = [];
@@ -235,24 +232,19 @@ export const energySourcesByType = (prefs: EnergyPreferences) =>
export interface EnergyData { export interface EnergyData {
start: Date; start: Date;
end?: Date; end?: Date;
startCompare?: Date;
endCompare?: Date;
prefs: EnergyPreferences; prefs: EnergyPreferences;
info: EnergyInfo; info: EnergyInfo;
stats: Statistics; stats: Statistics;
statsCompare: Statistics;
co2SignalConfigEntry?: ConfigEntry; co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string; co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption; fossilEnergyConsumption?: FossilEnergyConsumption;
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
} }
const getEnergyData = async ( const getEnergyData = async (
hass: HomeAssistant, hass: HomeAssistant,
prefs: EnergyPreferences, prefs: EnergyPreferences,
start: Date, start: Date,
end?: Date, end?: Date
compare?: boolean
): Promise<EnergyData> => { ): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([ const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass, { domain: "co2signal" }), getConfigEntries(hass, { domain: "co2signal" }),
@@ -358,8 +350,6 @@ const getEnergyData = async (
} }
const dayDifference = differenceInDays(end || new Date(), start); const dayDifference = differenceInDays(end || new Date(), start);
const period =
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
// Subtract 1 hour from start to get starting point data // Subtract 1 hour from start to get starting point data
const startMinHour = addHours(start, -1); const startMinHour = addHours(start, -1);
@@ -369,34 +359,10 @@ const getEnergyData = async (
startMinHour, startMinHour,
end, end,
statIDs, statIDs,
period dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
); );
let statsCompare;
let startCompare;
let endCompare;
if (compare) {
if (dayDifference > 27 && dayDifference < 32) {
// When comparing a month, we want to start at the begining of the month
startCompare = addMonths(start, -1);
} else {
startCompare = addDays(start, (dayDifference + 1) * -1);
}
const compareStartMinHour = addHours(startCompare, -1);
endCompare = addMilliseconds(start, -1);
statsCompare = await fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
statIDs,
period
);
}
let fossilEnergyConsumption: FossilEnergyConsumption | undefined; let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
if (co2SignalEntity !== undefined) { if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption( fossilEnergyConsumption = await getFossilEnergyConsumption(
@@ -407,16 +373,6 @@ const getEnergyData = async (
end, end,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour" dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
); );
if (compare) {
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
hass!,
startCompare,
consumptionStatIDs,
co2SignalEntity,
endCompare,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
}
} }
Object.values(stats).forEach((stat) => { Object.values(stats).forEach((stat) => {
@@ -432,19 +388,15 @@ const getEnergyData = async (
} }
}); });
const data: EnergyData = { const data = {
start, start,
end, end,
startCompare,
endCompare,
info, info,
prefs, prefs,
stats, stats,
statsCompare,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
fossilEnergyConsumption, fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
}; };
return data; return data;
@@ -453,11 +405,9 @@ const getEnergyData = async (
export interface EnergyCollection extends Collection<EnergyData> { export interface EnergyCollection extends Collection<EnergyData> {
start: Date; start: Date;
end?: Date; end?: Date;
compare?: boolean;
prefs?: EnergyPreferences; prefs?: EnergyPreferences;
clearPrefs(): void; clearPrefs(): void;
setPeriod(newStart: Date, newEnd?: Date): void; setPeriod(newStart: Date, newEnd?: Date): void;
setCompare(compare: boolean): void;
_refreshTimeout?: number; _refreshTimeout?: number;
_updatePeriodTimeout?: number; _updatePeriodTimeout?: number;
_active: number; _active: number;
@@ -528,8 +478,7 @@ export const getEnergyDataCollection = (
hass, hass,
collection.prefs, collection.prefs,
collection.start, collection.start,
collection.end, collection.end
collection.compare
); );
} }
) as EnergyCollection; ) as EnergyCollection;
@@ -585,9 +534,6 @@ export const getEnergyDataCollection = (
collection._updatePeriodTimeout = undefined; collection._updatePeriodTimeout = undefined;
} }
}; };
collection.setCompare = (compare: boolean) => {
collection.compare = compare;
};
return collection; return collection;
}; };

View File

@@ -20,20 +20,3 @@ export const BOARD_NAMES: Record<string, string> = {
"intel-nuc": "Intel NUC", "intel-nuc": "Intel NUC",
yellow: "Home Assistant Yellow", yellow: "Home Assistant Yellow",
}; };
export interface HardwareInfo {
hardware: HardwareInfoEntry[];
}
export interface HardwareInfoEntry {
board: HardwareInfoBoardInfo;
name: string;
url?: string;
}
export interface HardwareInfoBoardInfo {
manufacturer: string;
model?: string;
revision?: string;
hassio_board_id?: string;
}

View File

@@ -179,10 +179,7 @@ export const fetchHassioInfo = async (
}; };
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) => export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
hass.callApi<string>( hass.callApi<string>("GET", `hassio/${provider}/logs`);
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
);
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,7 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation"; import { FrontendLocaleData } from "./translation";
@@ -26,7 +27,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
export interface LineChartState { export interface LineChartState {
state: string; state: string;
last_changed: number; last_changed: string;
attributes?: Record<string, any>; attributes?: Record<string, any>;
} }
@@ -46,7 +47,7 @@ export interface LineChartUnit {
export interface TimelineState { export interface TimelineState {
state_localize: string; state_localize: string;
state: string; state: string;
last_changed: number; last_changed: string;
} }
export interface TimelineEntity { export interface TimelineEntity {
@@ -140,21 +141,6 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[]; [statisticId: string]: StatisticsValidationResult[];
} }
export interface HistoryStates {
[entityId: string]: EntityHistoryState[];
}
interface EntityHistoryState {
/** state */
s: string;
/** attributes */
a: { [key: string]: any };
/** last_changed; if set, also applies to lu */
lc: number;
/** last_updated */
lu: number;
}
export const entityIdHistoryNeedsAttributes = ( export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string
@@ -195,27 +181,6 @@ export const fetchRecent = (
return hass.callApi("GET", url); return hass.callApi("GET", url);
}; };
export const fetchRecentWS = (
hass: HomeAssistant,
entityId: string, // This may be CSV
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true,
noAttributes?: boolean
) =>
hass.callWS<HistoryStates>({
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
significant_changes_only: significantChangesOnly || false,
include_start_time_state: !skipInitialState,
minimal_response: minimalResponse,
no_attributes: noAttributes || false,
entity_ids: entityId.split(","),
});
export const fetchDate = ( export const fetchDate = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
@@ -233,27 +198,6 @@ export const fetchDate = (
}` }`
); );
export const fetchDateWS = (
hass: HomeAssistant,
startTime: Date,
endTime: Date,
entityId?: string
) => {
const params = {
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
minimal_response: true,
no_attributes: !!(
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
),
};
if (entityId) {
return hass.callWS<HistoryStates>({ ...params, entity_ids: [entityId] });
}
return hass.callWS<HistoryStates>(params);
};
const equalState = (obj1: LineChartState, obj2: LineChartState) => const equalState = (obj1: LineChartState, obj2: LineChartState) =>
obj1.state === obj2.state && obj1.state === obj2.state &&
// Only compare attributes if both states have an attributes object. // Only compare attributes if both states have an attributes object.
@@ -268,47 +212,46 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = ( const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: FrontendLocaleData, language: FrontendLocaleData,
entityId: string, states: HassEntity[]
states: EntityHistoryState[]
): TimelineEntity => { ): TimelineEntity => {
const data: TimelineState[] = []; const data: TimelineState[] = [];
const first: EntityHistoryState = states[0]; const last_element = states.length - 1;
for (const state of states) { for (const state of states) {
if (data.length > 0 && state.s === data[data.length - 1].state) { if (data.length > 0 && state.state === data[data.length - 1].state) {
continue; continue;
} }
// Copy the data from the last element as its the newest
// and is only needed to localize the data
if (!state.entity_id) {
state.attributes = states[last_element].attributes;
state.entity_id = states[last_element].entity_id;
}
data.push({ data.push({
state_localize: computeStateDisplayFromEntityAttributes( state_localize: computeStateDisplay(localize, state, language),
localize, state: state.state,
language, last_changed: state.last_changed,
entityId,
state.a || first.a,
state.s
),
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
}); });
} }
return { return {
name: computeStateNameFromEntityAttributes(entityId, states[0].a), name: computeStateName(states[0]),
entity_id: entityId, entity_id: states[0].entity_id,
data, data,
}; };
}; };
const processLineChartEntities = ( const processLineChartEntities = (
unit, unit,
entities: HistoryStates entities: HassEntity[][]
): LineChartUnit => { ): LineChartUnit => {
const data: LineChartEntity[] = []; const data: LineChartEntity[] = [];
Object.keys(entities).forEach((entityId) => { for (const states of entities) {
const states = entities[entityId]; const last: HassEntity = states[states.length - 1];
const first: EntityHistoryState = states[0]; const domain = computeStateDomain(last);
const domain = computeDomain(entityId);
const processedStates: LineChartState[] = []; const processedStates: LineChartState[] = [];
for (const state of states) { for (const state of states) {
@@ -316,24 +259,18 @@ const processLineChartEntities = (
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) { if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
processedState = { processedState = {
state: state.s, state: state.state,
last_changed: state.lu * 1000, last_changed: state.last_updated,
attributes: {}, attributes: {},
}; };
for (const attr of LINE_ATTRIBUTES_TO_KEEP) { for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
if (attr in state.a) { if (attr in state.attributes) {
processedState.attributes![attr] = state.a[attr]; processedState.attributes![attr] = state.attributes[attr];
} }
} }
} else { } else {
processedState = { processedState = state;
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
attributes: {},
};
} }
if ( if (
@@ -352,53 +289,52 @@ const processLineChartEntities = (
data.push({ data.push({
domain, domain,
name: computeStateNameFromEntityAttributes(entityId, first.a), name: computeStateName(last),
entity_id: entityId, entity_id: last.entity_id,
states: processedStates, states: processedStates,
}); });
}); }
return { return {
unit, unit,
identifier: Object.keys(entities).join(""), identifier: entities.map((states) => states[0].entity_id).join(""),
data, data,
}; };
}; };
const stateUsesUnits = (state: HassEntity) => const stateUsesUnits = (state: HassEntity) =>
attributesHaveUnits(state.attributes); "unit_of_measurement" in state.attributes ||
"state_class" in state.attributes;
const attributesHaveUnits = (attributes: { [key: string]: any }) =>
"unit_of_measurement" in attributes || "state_class" in attributes;
export const computeHistory = ( export const computeHistory = (
hass: HomeAssistant, hass: HomeAssistant,
stateHistory: HistoryStates, stateHistory: HassEntity[][],
localize: LocalizeFunc localize: LocalizeFunc
): HistoryResult => { ): HistoryResult => {
const lineChartDevices: { [unit: string]: HistoryStates } = {}; const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
const timelineDevices: TimelineEntity[] = []; const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) { if (!stateHistory) {
return { line: [], timeline: [] }; return { line: [], timeline: [] };
} }
Object.keys(stateHistory).forEach((entityId) => {
const stateInfo = stateHistory[entityId]; stateHistory.forEach((stateInfo) => {
if (stateInfo.length === 0) { if (stateInfo.length === 0) {
return; return;
} }
const entityId = stateInfo[0].entity_id;
const currentState = const currentState =
entityId in hass.states ? hass.states[entityId] : undefined; entityId in hass.states ? hass.states[entityId] : undefined;
const stateWithUnitorStateClass = const stateWithUnitorStateClass =
!currentState && !currentState &&
stateInfo.find((state) => state.a && attributesHaveUnits(state.a)); stateInfo.find((state) => state.attributes && stateUsesUnits(state));
let unit: string | undefined; let unit: string | undefined;
if (currentState && stateUsesUnits(currentState)) { if (currentState && stateUsesUnits(currentState)) {
unit = currentState.attributes.unit_of_measurement || " "; unit = currentState.attributes.unit_of_measurement || " ";
} else if (stateWithUnitorStateClass) { } else if (stateWithUnitorStateClass) {
unit = stateWithUnitorStateClass.a.unit_of_measurement || " "; unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
} else { } else {
unit = { unit = {
climate: hass.config.unit_system.temperature, climate: hass.config.unit_system.temperature,
@@ -412,15 +348,12 @@ export const computeHistory = (
if (!unit) { if (!unit) {
timelineDevices.push( timelineDevices.push(
processTimelineEntity(localize, hass.locale, entityId, stateInfo) processTimelineEntity(localize, hass.locale, stateInfo)
); );
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) { } else if (unit in lineChartDevices) {
lineChartDevices[unit][entityId].push(...stateInfo); lineChartDevices[unit].push(stateInfo);
} else { } else {
if (!(unit in lineChartDevices)) { lineChartDevices[unit] = [stateInfo];
lineChartDevices[unit] = {};
}
lineChartDevices[unit][entityId] = stateInfo;
} }
}); });

View File

@@ -42,18 +42,8 @@ export const domainToName = (
manifest?: IntegrationManifest manifest?: IntegrationManifest
) => localize(`component.${domain}.title`) || manifest?.name || domain; ) => localize(`component.${domain}.title`) || manifest?.name || domain;
export const fetchIntegrationManifests = ( export const fetchIntegrationManifests = (hass: HomeAssistant) =>
hass: HomeAssistant, hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
integrations?: string[]
) => {
const params: any = {
type: "manifest/list",
};
if (integrations) {
params.integrations = integrations;
}
return hass.callWS<IntegrationManifest[]>(params);
};
export const fetchIntegrationManifest = ( export const fetchIntegrationManifest = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,9 +1,5 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
BINARY_STATE_OFF,
BINARY_STATE_ON,
DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
@@ -13,51 +9,25 @@ import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"]; export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookStreamMessage {
events: LogbookEntry[];
start_time?: number; // Start time of this historical chunk
end_time?: number; // End time of this historical chunk
partial?: boolean; // Indiciates more historical chunks are coming
}
export interface LogbookEntry { export interface LogbookEntry {
// Base data when: string;
when: number; // Python timestamp. Do *1000 to get JS timestamp.
name: string; name: string;
message?: string; message?: string;
entity_id?: string; entity_id?: string;
icon?: string; icon?: string;
source?: string; // The trigger source source?: string;
domain?: string; domain?: string;
state?: string; // The state of the entity
// Context data
context_id?: string; context_id?: string;
context_user_id?: string; context_user_id?: string;
context_event_type?: string; context_event_type?: string;
context_domain?: string; context_domain?: string;
context_service?: string; // Service calls only context_service?: string;
context_entity_id?: string; context_entity_id?: string;
context_entity_id_name?: string; // Legacy, not longer sent context_entity_id_name?: string;
context_name?: string; context_name?: string;
context_state?: string; // The state of the entity state?: string;
context_source?: string; // The trigger source
context_message?: string;
} }
//
// Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers
//
const triggerPhrases = {
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
"state of": "triggered_by_state_of", // state trigger
event: "triggered_by_event", // event trigger
time: "triggered_by_time", // time trigger
"time pattern": "triggered_by_time_pattern", // time trigger
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
};
const DATA_CACHE: { const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> }; [cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {}; } = {};
@@ -67,13 +37,18 @@ export const getLogbookDataForContext = async (
startDate: string, startDate: string,
contextId?: string contextId?: string
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
await hass.loadBackendTranslation("device_class"); const localize = await hass.loadBackendTranslation("device_class");
return getLogbookDataFromServer( return addLogbookMessage(
hass, hass,
startDate, localize,
undefined, await getLogbookDataFromServer(
undefined, hass,
contextId startDate,
undefined,
undefined,
undefined,
contextId
)
); );
}; };
@@ -81,123 +56,107 @@ export const getLogbookData = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityIds?: string[], entityId?: string,
deviceIds?: string[] entity_matches_only?: boolean
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
await hass.loadBackendTranslation("device_class"); const localize = await hass.loadBackendTranslation("device_class");
return deviceIds?.length return addLogbookMessage(
? getLogbookDataFromServer( hass,
hass, localize,
startDate, await getLogbookDataCache(
endDate, hass,
entityIds, startDate,
undefined, endDate,
deviceIds entityId,
) entity_matches_only
: getLogbookDataCache(hass, startDate, endDate, entityIds); )
);
}; };
const getLogbookDataCache = async ( export const addLogbookMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
logbookData: LogbookEntry[]
): LogbookEntry[] => {
for (const entry of logbookData) {
const stateObj = hass!.states[entry.entity_id!];
if (entry.state && stateObj) {
entry.message = getLogbookMessage(
hass,
localize,
entry.state,
stateObj,
computeDomain(entry.entity_id!)
);
}
}
return logbookData;
};
export const getLogbookDataCache = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string[] entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const ALL_ENTITIES = "*"; const ALL_ENTITIES = "*";
const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES; if (!entityId) {
entityId = ALL_ENTITIES;
}
const cacheKey = `${startDate}${endDate}`; const cacheKey = `${startDate}${endDate}`;
if (!DATA_CACHE[cacheKey]) { if (!DATA_CACHE[cacheKey]) {
DATA_CACHE[cacheKey] = {}; DATA_CACHE[cacheKey] = {};
} }
if (entityIdKey in DATA_CACHE[cacheKey]) { if (entityId in DATA_CACHE[cacheKey]) {
return DATA_CACHE[cacheKey][entityIdKey]; return DATA_CACHE[cacheKey][entityId];
} }
if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) { if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]; const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
return entities.filter( return entities.filter((entity) => entity.entity_id === entityId);
(entity) => entity.entity_id && entityId.includes(entity.entity_id)
);
} }
DATA_CACHE[cacheKey][entityIdKey] = getLogbookDataFromServer( DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
hass, hass,
startDate, startDate,
endDate, endDate,
entityId entityId !== ALL_ENTITIES ? entityId : undefined,
); entity_matches_only
return DATA_CACHE[cacheKey][entityIdKey]; ).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
}; };
const getLogbookDataFromServer = ( const getLogbookDataFromServer = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate?: string, endDate?: string,
entityIds?: string[], entityId?: string,
contextId?: string, entitymatchesOnly?: boolean,
deviceIds?: string[] contextId?: string
): Promise<LogbookEntry[]> => { ) => {
// If all specified filters are empty lists, we can return an empty list. const params = new URLSearchParams();
if (
(entityIds || deviceIds) &&
(!entityIds || entityIds.length === 0) &&
(!deviceIds || deviceIds.length === 0)
) {
return Promise.resolve([]);
}
const params: any = {
type: "logbook/get_events",
start_time: startDate,
};
if (endDate) { if (endDate) {
params.end_time = endDate; params.append("end_time", endDate);
} }
if (entityIds?.length) { if (entityId) {
params.entity_ids = entityIds; params.append("entity", entityId);
} }
if (deviceIds?.length) { if (entitymatchesOnly) {
params.device_ids = deviceIds; params.append("entity_matches_only", "");
} }
if (contextId) { if (contextId) {
params.context_id = contextId; params.append("context_id", contextId);
} }
return hass.callWS<LogbookEntry[]>(params);
};
export const subscribeLogbook = ( return hass.callApi<LogbookEntry[]>(
hass: HomeAssistant, "GET",
callbackFunction: (message: LogbookStreamMessage) => void, `logbook/${startDate}?${params.toString()}`
startDate: string,
endDate: string,
entityIds?: string[],
deviceIds?: string[]
): Promise<UnsubscribeFunc> => {
// If all specified filters are empty lists, we can return an empty list.
if (
(entityIds || deviceIds) &&
(!entityIds || entityIds.length === 0) &&
(!deviceIds || deviceIds.length === 0)
) {
return Promise.reject("No entities or devices");
}
const params: any = {
type: "logbook/event_stream",
start_time: startDate,
end_time: endDate,
};
if (entityIds?.length) {
params.entity_ids = entityIds;
}
if (deviceIds?.length) {
params.device_ids = deviceIds;
}
return hass.connection.subscribeMessage<LogbookStreamMessage>(
(message) => callbackFunction(message),
params
); );
}; };
@@ -205,49 +164,7 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; DATA_CACHE[`${startDate}${endDate}`] = {};
}; };
export const createHistoricState = ( export const getLogbookMessage = (
currentStateObj: HassEntity,
state?: string
): HassEntity => <HassEntity>(<unknown>{
entity_id: currentStateObj.entity_id,
state: state,
attributes: {
// Rebuild the historical state by copying static attributes only
device_class: currentStateObj?.attributes.device_class,
source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time,
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
// as they would present a false state in the log (played media right now vs actual historic data).
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture_local,
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj?.attributes.entity_picture,
},
});
export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhrase in triggerPhrases) {
if (source.startsWith(triggerPhrase)) {
return source.replace(
triggerPhrase,
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
);
}
}
return source;
};
export const localizeStateMessage = (
hass: HomeAssistant, hass: HomeAssistant,
localize: LocalizeFunc, localize: LocalizeFunc,
state: string, state: string,

View File

@@ -131,9 +131,9 @@ export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service"; action: "call-service";
service: string; service: string;
target?: HassServiceTarget; target?: HassServiceTarget;
// "service_data" is kept for backwards compatibility. Replaced by "data". service_data?: {
service_data?: Record<string, unknown>; [key: string]: any;
data?: Record<string, unknown>; };
} }
export interface NavigateActionConfig extends BaseActionConfig { export interface NavigateActionConfig extends BaseActionConfig {
@@ -159,7 +159,6 @@ export interface CustomActionConfig extends BaseActionConfig {
} }
export interface BaseActionConfig { export interface BaseActionConfig {
action: string;
confirmation?: ConfirmationRestrictionConfig; confirmation?: ConfirmationRestrictionConfig;
} }

View File

@@ -47,17 +47,12 @@ export interface SceneConfig {
name: string; name: string;
icon?: string; icon?: string;
entities: SceneEntities; entities: SceneEntities;
metadata?: SceneMetaData;
} }
export interface SceneEntities { export interface SceneEntities {
[entityId: string]: string | { state: string; [key: string]: any }; [entityId: string]: string | { state: string; [key: string]: any };
} }
export interface SceneMetaData {
[entityId: string]: { entity_only?: boolean | undefined };
}
export const activateScene = ( export const activateScene = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string

View File

@@ -52,7 +52,7 @@ export const getHassTranslations = async (
hass: HomeAssistant, hass: HomeAssistant,
language: string, language: string,
category: TranslationCategory, category: TranslationCategory,
integration?: string | string[], integration?: string,
config_flow?: boolean config_flow?: boolean
): Promise<Record<string, unknown>> => { ): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({ const result = await hass.callWS<{ resources: Record<string, unknown> }>({

View File

@@ -2,15 +2,10 @@ import type {
HassEntities, HassEntities,
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { BINARY_STATE_ON } from "../common/const"; import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { import { supportsFeature } from "../common/entity/supports-feature";
supportsFeature,
supportsFeatureFromAttributes,
} from "../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@@ -38,13 +33,8 @@ export interface UpdateEntity extends HassEntityBase {
} }
export const updateUsesProgress = (entity: UpdateEntity): boolean => export const updateUsesProgress = (entity: UpdateEntity): boolean =>
updateUsesProgressFromAttributes(entity.attributes); supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateUsesProgressFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
typeof attributes.in_progress === "number";
export const updateCanInstall = ( export const updateCanInstall = (
entity: UpdateEntity, entity: UpdateEntity,
@@ -57,11 +47,6 @@ export const updateCanInstall = (
export const updateIsInstalling = (entity: UpdateEntity): boolean => export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress; updateUsesProgress(entity) || !!entity.attributes.in_progress;
export const updateIsInstallingFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({ hass.callWS<string | null>({
type: "update/release_notes", type: "update/release_notes",
@@ -125,32 +110,15 @@ export const checkForEntityUpdates = async (
return; return;
} }
let updated = 0;
const unsubscribeEvents = await hass.connection.subscribeEvents<HassEvent>(
(event) => {
if (computeDomain(event.data.entity_id) === "update") {
updated++;
showToast(element, {
message: hass.localize("ui.panel.config.updates.updates_refreshed", {
count: updated,
}),
});
}
},
"state_changed"
);
await hass.callService("homeassistant", "update_entity", { await hass.callService("homeassistant", "update_entity", {
entity_id: entities, entity_id: entities,
}); });
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now... if (filterUpdateEntitiesWithInstall(hass.states).length) {
await new Promise((r) => setTimeout(r, 10000)); showToast(element, {
message: hass.localize("ui.panel.config.updates.updates_refreshed"),
unsubscribeEvents(); });
} else {
if (updated === 0) {
showToast(element, { showToast(element, {
message: hass.localize("ui.panel.config.updates.no_new_updates"), message: hass.localize("ui.panel.config.updates.no_new_updates"),
}); });

View File

@@ -2,21 +2,9 @@ import {
mdiAlertCircleOutline, mdiAlertCircleOutline,
mdiGauge, mdiGauge,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog, mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherNightPartlyCloudy,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy, mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy, mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js"; } from "@mdi/js";
import { import {
HassEntityAttributeBase, HassEntityAttributeBase,
@@ -69,21 +57,7 @@ export const weatherSVGs = new Set<string>([
]); ]);
export const weatherIcons = { export const weatherIcons = {
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline, exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
}; };
export const weatherAttrIcons = { export const weatherAttrIcons = {
@@ -463,13 +437,6 @@ export const getWeatherStateIcon = (
return undefined; return undefined;
}; };
export const weatherIcon = (state?: string, nightTime?: boolean): string =>
!state
? undefined
: nightTime && state === "partlycloudy"
? mdiWeatherNightPartlyCloudy
: weatherIcons[state];
const DAY_IN_MILLISECONDS = 86400000; const DAY_IN_MILLISECONDS = 86400000;
export const isForecastHourly = ( export const isForecastHourly = (

View File

@@ -145,7 +145,7 @@ export interface ZWaveJSController {
supports_timers: boolean; supports_timers: boolean;
is_heal_network_active: boolean; is_heal_network_active: boolean;
inclusion_state: InclusionState; inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[]; nodes: number[];
} }
export interface ZWaveJSNodeStatus { export interface ZWaveJSNodeStatus {
@@ -167,9 +167,6 @@ export interface ZwaveJSNodeMetadata {
wakeup: string; wakeup: string;
reset: string; reset: string;
device_database_url: string; device_database_url: string;
}
export interface ZwaveJSNodeComments {
comments: ZWaveJSNodeComment[]; comments: ZWaveJSNodeComment[];
} }
@@ -203,7 +200,8 @@ export interface ZWaveJSNodeConfigParamMetadata {
export interface ZWaveJSSetConfigParamData { export interface ZWaveJSSetConfigParamData {
type: string; type: string;
device_id: string; entry_id: string;
node_id: number;
property: number; property: number;
property_key?: number; property_key?: number;
value: string | number; value: string | number;
@@ -230,20 +228,6 @@ export interface ZWaveJSHealNetworkStatusMessage {
heal_node_status: { [key: number]: string }; heal_node_status: { [key: number]: string };
} }
export interface ZWaveJSControllerStatisticsUpdatedMessage {
event: "statistics updated";
source: "controller";
messages_tx: number;
messages_rx: number;
messages_dropped_tx: number;
messages_dropped_rx: number;
nak: number;
can: number;
timeout_ack: number;
timeout_response: number;
timeout_callback: number;
}
export interface ZWaveJSRemovedNode { export interface ZWaveJSRemovedNode {
node_id: number; node_id: number;
manufacturer: string; manufacturer: string;
@@ -301,23 +285,12 @@ export const migrateZwave = (
export const fetchZwaveNetworkStatus = ( export const fetchZwaveNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_or_entry_id: { entry_id: string
device_id?: string; ): Promise<ZWaveJSNetwork> =>
entry_id?: string; hass.callWS({
}
): Promise<ZWaveJSNetwork> => {
if (device_or_entry_id.device_id && device_or_entry_id.entry_id) {
throw new Error("Only one of device or entry ID should be supplied.");
}
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
throw new Error("Either device or entry ID should be supplied.");
}
return hass.callWS({
type: "zwave_js/network_status", type: "zwave_js/network_status",
device_id: device_or_entry_id.device_id, entry_id,
entry_id: device_or_entry_id.entry_id,
}); });
};
export const fetchZwaveDataCollectionStatus = ( export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant, hass: HomeAssistant,
@@ -454,50 +427,49 @@ export const unprovisionZwaveSmartStartNode = (
export const fetchZwaveNodeStatus = ( export const fetchZwaveNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZWaveJSNodeStatus> => ): Promise<ZWaveJSNodeStatus> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_status", type: "zwave_js/node_status",
device_id, entry_id,
node_id,
}); });
export const fetchZwaveNodeMetadata = ( export const fetchZwaveNodeMetadata = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZwaveJSNodeMetadata> => ): Promise<ZwaveJSNodeMetadata> =>
hass.callWS({ hass.callWS({
type: "zwave_js/node_metadata", type: "zwave_js/node_metadata",
device_id, entry_id,
}); node_id,
export const fetchZwaveNodeComments = (
hass: HomeAssistant,
device_id: string
): Promise<ZwaveJSNodeComments> =>
hass.callWS({
type: "zwave_js/node_comments",
device_id,
}); });
export const fetchZwaveNodeConfigParameters = ( export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<ZWaveJSNodeConfigParams> => ): Promise<ZWaveJSNodeConfigParams> =>
hass.callWS({ hass.callWS({
type: "zwave_js/get_config_parameters", type: "zwave_js/get_config_parameters",
device_id, entry_id,
node_id,
}); });
export const setZwaveNodeConfigParameter = ( export const setZwaveNodeConfigParameter = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
property: number, property: number,
value: number, value: number,
property_key?: number property_key?: number
): Promise<ZWaveJSSetConfigParamResult> => { ): Promise<ZWaveJSSetConfigParamResult> => {
const data: ZWaveJSSetConfigParamData = { const data: ZWaveJSSetConfigParamData = {
type: "zwave_js/set_config_parameter", type: "zwave_js/set_config_parameter",
device_id, entry_id,
node_id,
property, property,
value, value,
property_key, property_key,
@@ -507,36 +479,42 @@ export const setZwaveNodeConfigParameter = (
export const reinterviewZwaveNode = ( export const reinterviewZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/refresh_node_info", type: "zwave_js/refresh_node_info",
device_id, entry_id,
node_id,
} }
); );
export const healZwaveNode = ( export const healZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string entry_id: string,
node_id: number
): Promise<boolean> => ): Promise<boolean> =>
hass.callWS({ hass.callWS({
type: "zwave_js/heal_node", type: "zwave_js/heal_node",
device_id, entry_id,
node_id,
}); });
export const removeFailedZwaveNode = ( export const removeFailedZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message: any) => void callbackFunction: (message: any) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/remove_failed_node", type: "zwave_js/remove_failed_node",
device_id, entry_id,
node_id,
} }
); );
@@ -560,14 +538,16 @@ export const stopHealZwaveNetwork = (
export const subscribeZwaveNodeReady = ( export const subscribeZwaveNodeReady = (
hass: HomeAssistant, hass: HomeAssistant,
device_id: string, entry_id: string,
node_id: number,
callbackFunction: (message) => void callbackFunction: (message) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage( hass.connection.subscribeMessage(
(message: any) => callbackFunction(message), (message: any) => callbackFunction(message),
{ {
type: "zwave_js/node_ready", type: "zwave_js/node_ready",
device_id, entry_id,
node_id,
} }
); );
@@ -584,19 +564,6 @@ export const subscribeHealZwaveNetworkProgress = (
} }
); );
export const subscribeZwaveControllerStatistics = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSControllerStatisticsUpdatedMessage) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_controller_statistics",
entry_id,
}
);
export const getZwaveJsIdentifiersFromDevice = ( export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => { ): ZWaveJSNodeIdentifiers | undefined => {

View File

@@ -309,7 +309,7 @@ class DataEntryFlowDialog extends LitElement {
: this._step.type === "abort" : this._step.type === "abort"
? html` ? html`
<step-flow-abort <step-flow-abort
.params=${this._params} .flowConfig=${this._params.flowConfig}
.step=${this._step} .step=${this._step}
.hass=${this.hass} .hass=${this.hass}
.domain=${this._step.handler} .domain=${this._step.handler}
@@ -518,9 +518,10 @@ class DataEntryFlowDialog extends LitElement {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
inset-inline-start: initial; }
inset-inline-end: 0px; :host-context([style*="direction: rtl;"]) .dialog-actions {
direction: var(--direction); right: auto;
left: 0;
} }
.dialog-actions > * { .dialog-actions > * {
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@@ -131,7 +131,6 @@ export interface DataEntryFlowDialogParams {
}) => void; }) => void;
flowConfig: FlowConfig; flowConfig: FlowConfig;
showAdvanced?: boolean; showAdvanced?: boolean;
dialogParentElement?: HTMLElement;
} }
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow"); export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
@@ -147,7 +146,6 @@ export const showFlowDialog = (
dialogParams: { dialogParams: {
...dialogParams, ...dialogParams,
flowConfig, flowConfig,
dialogParentElement: element,
}, },
}); });
}; };

View File

@@ -1,25 +1,15 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { DataEntryFlowStepAbort } from "../../data/data_entry_flow";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential"; import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { domainToName } from "../../data/integration";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import { showConfigFlowDialog } from "./show-dialog-config-flow";
@customElement("step-flow-abort") @customElement("step-flow-abort")
class StepFlowAbort extends LitElement { class StepFlowAbort extends LitElement {
@property({ attribute: false }) public params!: DataEntryFlowDialogParams; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -27,21 +17,11 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public domain!: string; @property({ attribute: false }) public domain!: string;
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
if (this.step.reason === "missing_credentials") {
this._handleMissingCreds();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.step.reason === "missing_credentials") {
return html``;
}
return html` return html`
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2> <h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
<div class="content"> <div class="content">
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)} ${this.flowConfig.renderAbortDescription(this.hass, this.step)}
</div> </div>
<div class="buttons"> <div class="buttons">
<mwc-button @click=${this._flowDone} <mwc-button @click=${this._flowDone}
@@ -53,32 +33,6 @@ class StepFlowAbort extends LitElement {
`; `;
} }
private async _handleMissingCreds() {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.missing_credentials",
{
integration: domainToName(this.hass.localize, this.domain),
}
),
});
this._flowDone();
if (!confirm) {
return;
}
// Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain,
applicationCredentialAddedCallback: () => {
showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
});
},
});
}
private _flowDone(): void { private _flowDone(): void {
fireEvent(this, "flow-update", { step: undefined }); fireEvent(this, "flow-update", { step: undefined });
} }

View File

@@ -192,8 +192,11 @@ class StepFlowForm extends LitElement {
} }
h2 { h2 {
word-break: break-word; word-break: break-word;
padding-inline-end: 72px; padding-right: 72px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 72px !important;
} }
`, `,
]; ];

View File

@@ -104,8 +104,11 @@ class StepFlowPickFlow extends LitElement {
margin: 16px 0; margin: 16px 0;
} }
h2 { h2 {
padding-inline-end: 66px; padding-right: 66px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 66px !important;
} }
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
div { div {

View File

@@ -311,8 +311,11 @@ class StepFlowPickHandler extends LitElement {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
h2 { h2 {
padding-inline-end: 66px; padding-right: 66px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) h2 {
padding-right: auto !important;
padding-left: 66px !important;
} }
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
mwc-list { mwc-list {

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