Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
29a638c56e Open more info as default action in entity heading badge 2024-09-30 11:08:26 +02:00
235 changed files with 4875 additions and 7505 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.0
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.1.cjs yarnPath: .yarn/releases/yarn-4.5.0.cjs

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list"; import { ActionDetail } from "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import { Auth, Connection } from "home-assistant-js-websocket"; import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
@@ -90,8 +89,8 @@ class HcCast extends LitElement {
generateDefaultViewConfig({}, {}, {}, {}, () => ""), generateDefaultViewConfig({}, {}, {}, {}, () => ""),
] ]
).map( ).map(
(view, idx) => html` (view, idx) =>
<ha-list-item html`<ha-list-item
graphic="avatar" graphic="avatar"
.activated=${this.castManager.status?.lovelacePath === .activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)} (view.path ?? idx)}
@@ -109,9 +108,8 @@ class HcCast extends LitElement {
: html`<ha-svg-icon : html`<ha-svg-icon
slot="item-icon" slot="item-icon"
.path=${mdiViewDashboard} .path=${mdiViewDashboard}
></ha-svg-icon>`} ></ha-svg-icon>`}</ha-list-item
</ha-list-item> > `
`
)}</mwc-list )}</mwc-list
> >
`} `}

View File

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

View File

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

View File

@@ -111,16 +111,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature", friendly_name: "Living room Temperature",
}, },
}, },
"sensor.living_room_humidity": {
entity_id: "sensor.living_room_humidity",
state: "57",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Living room Humidity",
},
},
"sensor.outdoor_temperature": { "sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature", entity_id: "sensor.outdoor_temperature",
state: "10.5", state: "10.5",
@@ -199,14 +189,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32, supported_features: 32,
}, },
}, },
"binary_sensor.kitchen_motion": {
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
friendly_name: "Kitchen motion",
},
},
"light.worktop_spotlights": { "light.worktop_spotlights": {
entity_id: "light.worktop_spotlights", entity_id: "light.worktop_spotlights",
state: "off", state: "off",
@@ -441,14 +423,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 64063, supported_features: 64063,
}, },
}, },
"switch.in_meeting": {
entity_id: "switch.in_meeting",
state: "on",
attributes: {
icon: "mdi:laptop-account",
friendly_name: "In a meeting",
},
},
"sensor.standing_desk_height": { "sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height", entity_id: "sensor.standing_desk_height",
state: "72", state: "72",

View File

@@ -30,36 +30,12 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
? [] ? []
: [ : [
{ {
cards: [ title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
{ cards: [{ type: "custom:ha-demo-card" }],
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
}, },
]), ]),
{ {
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.living_room"
),
icon: "mdi:sofa",
badges: [
{
type: "entity",
entity: "sensor.living_room_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.living_room_humidity",
color: "indigo",
},
],
},
{ {
type: "tile", type: "tile",
entity: "light.floor_lamp", entity: "light.floor_lamp",
@@ -78,6 +54,13 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
type: "tile", type: "tile",
entity: "light.bar_lamp", entity: "light.bar_lamp",
}, },
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{ {
type: "tile", type: "tile",
entity: "cover.living_room_garden_shutter", entity: "cover.living_room_garden_shutter",
@@ -88,25 +71,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.living_room_nest_mini", entity: "media_player.living_room_nest_mini",
}, },
], ],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.kitchen"
),
icon: "mdi:fridge",
badges: [
{
type: "entity",
entity: "binary_sensor.kitchen_motion",
show_state: false,
color: "blue",
},
],
},
{ {
type: "tile", type: "tile",
entity: "cover.kitchen_shutter", entity: "cover.kitchen_shutter",
@@ -137,17 +106,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.kitchen_nest_audio", entity: "media_player.kitchen_nest_audio",
}, },
], ],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{ {
type: "tile", type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected", entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -185,17 +148,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey", color: "dark-grey",
}, },
], ],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{ {
type: "tile", type: "tile",
entity: "sun.sun", entity: "sun.sun",
@@ -228,38 +185,16 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"], state_content: ["preset_mode", "current_temperature"],
}, },
], ],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.study"
),
icon: "mdi:desk-lamp",
badges: [
{
type: "entity",
entity: "switch.in_meeting",
state: "on",
state_content: "name",
visibility: [
{
condition: "state",
state: "on",
entity: "switch.in_meeting",
},
],
},
],
},
{ {
type: "tile", type: "tile",
entity: "cover.study_shutter", entity: "cover.study_shutter",
name: "Shutter", name: "Shutter",
}, },
{ {
type: "tile", type: "tile",
entity: "light.study_spotlights", entity: "light.study_spotlights",
@@ -276,23 +211,12 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown", color: "brown",
icon: "mdi:desk", icon: "mdi:desk",
}, },
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
], ],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{ {
type: "tile", type: "tile",
entity: "light.outdoor_light", entity: "light.outdoor_light",
@@ -322,17 +246,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance", name: "Illuminance",
}, },
], ],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{ {
type: "tile", type: "tile",
entity: "automation.home_assistant_auto_update", entity: "automation.home_assistant_auto_update",
@@ -358,6 +276,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
}, },
], ],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
}, },
], ],
}, },

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], [], {}, this._action) ? describeAction(this.hass, [], [], this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span> <span>${describeAction(this.hass, [], [], conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,6 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup"; import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/hass-loading-screen";
type BackupItem = HassioBackup & { type BackupItem = HassioBackup & {
secondary: string; secondary: string;
@@ -70,8 +69,6 @@ export class HassioBackups extends LitElement {
@state() private _backups?: HassioBackup[] = []; @state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; private _dataTable!: HaTabsSubpageDataTable;
@@ -80,10 +77,15 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) { if (this.hass && this._firstUpdatedCalled) {
this.fetchBackups(); this.refreshData();
} }
} }
public async refreshData() {
await reloadHassioBackups(this.hass);
await this.fetchBackups();
}
private _computeBackupContent = (backup: HassioBackup): string => { private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") { if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup"); return this.supervisor.localize("backup.full_backup");
@@ -113,7 +115,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void { protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) { if (this.hass && this.isConnected) {
this.fetchBackups(); this.refreshData();
} }
this._firstUpdatedCalled = true; this._firstUpdatedCalled = true;
} }
@@ -173,13 +175,6 @@ export class HassioBackups extends LitElement {
if (!this.supervisor) { if (!this.supervisor) {
return nothing; return nothing;
} }
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5) .tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
@@ -286,7 +281,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) { private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
this.fetchBackups(); this.refreshData();
break; break;
case 1: case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor }); showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -311,15 +306,13 @@ export class HassioBackups extends LitElement {
supervisor: this.supervisor, supervisor: this.supervisor,
onDelete: () => this.fetchBackups(), onDelete: () => this.fetchBackups(),
}), }),
reloadBackup: () => this.fetchBackups(), reloadBackup: () => this.refreshData(),
}); });
} }
private async fetchBackups() { private async fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass); await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass); this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
} }
private async _deleteSelected() { private async _deleteSelected() {
@@ -346,7 +339,8 @@ export class HassioBackups extends LitElement {
}); });
return; return;
} }
await this.fetchBackups(); await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._dataTable.clearSelection(); this._dataTable.clearSelection();
} }

View File

@@ -13,11 +13,10 @@
<% for (const entry of es5EntryJS) { %> <% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>"); loadES5("<%= entry %>");
<% } %> <% } %>
}
} else { } else {
<% for (const entry of es5EntryJS) { %> <% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>"); loadES5("<%= entry %>");
<% } %> <% } %>
} }
}
})(); })();

View File

@@ -120,12 +120,10 @@ class HassioSupervisorLog extends LitElement {
this._error = undefined; this._error = undefined;
try { try {
const response = await fetchHassioLogs( this._content = await fetchHassioLogs(
this.hass, this.hass,
this._selectedLogProvider this._selectedLogProvider
); );
this._content = await response.text();
} catch (err: any) { } catch (err: any) {
this._error = this.supervisor.localize("system.log.get_logs", { this._error = this.supervisor.localize("system.log.get_logs", {
provider: this._selectedLogProvider, provider: this._selectedLogProvider,

View File

@@ -1,10 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -15,12 +16,12 @@ import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded"; import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import { import {
fetchHassioAddonChangelog, fetchHassioAddonChangelog,
fetchHassioAddonInfo, fetchHassioAddonInfo,
@@ -41,7 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
</ha-markdown> </ha-markdown>
</ha-faded> </ha-faded>
` `
: nothing} : ""}
<div class="versions"> <div class="versions">
<p> <p>
${this.supervisor.localize( ${this.supervisor.localize(
@@ -164,17 +164,15 @@ class UpdateAvailableCard extends LitElement {
</div> </div>
${["core", "addon"].includes(this._updateType) ${["core", "addon"].includes(this._updateType)
? html` ? html`
<hr /> <ha-formfield
<ha-settings-row> .label=${this.supervisor.localize(
<span slot="heading"> "update_available.create_backup"
${this.supervisor.localize( )}
"update_available.create_backup" >
)} <ha-checkbox checked></ha-checkbox>
</span> </ha-formfield>
<ha-switch id="create_backup" checked></ha-switch>
</ha-settings-row>
` `
: nothing} : ""}
` `
: html`<ha-circular-progress : html`<ha-circular-progress
aria-label="Updating" aria-label="Updating"
@@ -193,24 +191,22 @@ class UpdateAvailableCard extends LitElement {
? html` ? html`
<div class="card-actions"> <div class="card-actions">
${changelog ${changelog
? html` ? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<a href=${changelog} target="_blank" rel="noreferrer"> <mwc-button
<ha-button .label=${this.supervisor.localize(
.label=${this.supervisor.localize( "update_available.open_release_notes"
"update_available.open_release_notes" )}
)} >
> </mwc-button>
</ha-button> </a>`
</a> : ""}
`
: nothing}
<span></span> <span></span>
<ha-progress-button @click=${this._update}> <ha-progress-button @click=${this._update} raised>
${this.supervisor.localize("common.update")} ${this.supervisor.localize("common.update")}
</ha-progress-button> </ha-progress-button>
</div> </div>
` `
: nothing} : ""}
</ha-card> </ha-card>
`; `;
} }
@@ -246,11 +242,9 @@ class UpdateAvailableCard extends LitElement {
if (this._updateType && !["core", "addon"].includes(this._updateType)) { if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false; return false;
} }
const createBackupSwitch = this.shadowRoot?.getElementById( const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
"create-backup" if (checkbox) {
) as HaSwitch; return checkbox.checked;
if (createBackupSwitch) {
return createBackupSwitch.checked;
} }
return true; return true;
} }
@@ -403,50 +397,41 @@ class UpdateAvailableCard extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
haStyle, :host {
css` display: block;
:host { }
display: block; ha-card {
} margin: auto;
ha-card { }
margin: auto; a {
} text-decoration: none;
a { color: var(--primary-text-color);
text-decoration: none; }
color: var(--primary-text-color); ha-settings-row {
} padding: 0;
.card-actions { }
display: flex; .card-actions {
justify-content: space-between; display: flex;
} justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
margin: 32px; margin: 32px;
text-align: center; text-align: center;
} }
.progress-text { .progress-text {
text-align: center; text-align: center;
} }
ha-markdown { ha-markdown {
padding-bottom: 8px; padding-bottom: 8px;
} }
`;
ha-settings-row {
padding: 0;
margin-bottom: -16px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0 0 0;
}
`,
];
} }
} }

View File

@@ -25,24 +25,24 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.26.0", "@babel/runtime": "7.25.6",
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1", "@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.7.1", "@codemirror/commands": "6.6.2",
"@codemirror/language": "6.10.3", "@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1", "@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1", "@codemirror/view": "6.34.0",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.1", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.8.1", "@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-getcanonicallocales": "2.5.1", "@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.7.1", "@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "4.2.1", "@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.14.1", "@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.3.1", "@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.4.1", "@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.15", "@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15", "@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15", "@fullcalendar/interaction": "6.1.15",
@@ -86,11 +86,11 @@
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1", "@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2", "@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.1", "@vaadin/combo-box": "24.4.9",
"@vaadin/vaadin-themable-mixin": "24.5.1", "@vaadin/vaadin-themable-mixin": "24.4.9",
"@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",
@@ -98,13 +98,13 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.5", "chart.js": "4.4.4",
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.38.1", "core-js": "3.38.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"date-fns-tz": "3.2.0", "date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6", "dialog-polyfill": "0.5.6",
@@ -114,13 +114,13 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0", "home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.7.3", "intl-messageformat": "10.5.14",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.5.0", "luxon": "3.5.0",
"marked": "14.1.3", "marked": "14.1.2",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -151,15 +151,15 @@
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.26.0", "@babel/core": "7.25.2",
"@babel/helper-define-polyfill-provider": "0.6.2", "@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.25.9", "@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.9", "@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.26.0", "@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.26.0", "@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.16.0", "@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.8.0", "@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2", "@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2", "@octokit/rest": "21.0.2",
@@ -172,15 +172,15 @@
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17", "@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-sender": "1.0.10", "@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "2.0.0", "@types/color-name": "1.1.4",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.14", "@types/leaflet": "1.9.12",
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.9", "@types/mocha": "10.0.7",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8", "@types/sortablejs": "1.15.8",
@@ -194,18 +194,18 @@
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.2", "chai": "5.1.1",
"del": "8.0.0", "del": "7.1.0",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0", "eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9", "eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.30.0",
"eslint-plugin-lit": "1.15.0", "eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4", "eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4", "eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0", "eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "11.0.0", "glob": "11.0.0",
@@ -216,15 +216,15 @@
"gulp-zopfli-green": "6.0.2", "gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.1.6", "husky": "9.1.6",
"instant-mocha": "1.5.3", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.12", "magic-string": "0.30.11",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.7.3", "mocha": "10.5.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
@@ -233,14 +233,14 @@
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.6", "serve-handler": "6.1.5",
"sinon": "19.0.2", "sinon": "19.0.2",
"systemjs": "6.15.1", "systemjs": "6.15.1",
"tar": "7.4.3", "tar": "7.4.3",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.6.3", "typescript": "5.6.2",
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0", "webpack-dev-server": "5.1.0",
@@ -251,12 +251,12 @@
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0", "@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0", "lit": "2.8.0",
"clean-css": "5.3.3", "clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3", "@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15" "@fullcalendar/daygrid": "6.1.15"
}, },
"packageManager": "yarn@4.5.1" "packageManager": "yarn@4.5.0"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

View File

@@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["setuptools~=75.1"] requires = ["setuptools~=68.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20241010.0" version = "20240927.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -18,9 +18,5 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi fi
fi fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules # Install node modules
yarn install yarn install

View File

@@ -34,11 +34,9 @@ export const protocolIntegrationPicked = async (
if (domain === "zwave_js") { if (domain === "zwave_js") {
const entries = options?.config_entry const entries = options?.config_entry
? undefined ? undefined
: ( : await getConfigEntries(hass, {
await getConfigEntries(hass, { domain,
domain, });
})
).filter((e) => !e.disabled_by);
if ( if (
!isComponentLoaded(hass, "zwave_js") || !isComponentLoaded(hass, "zwave_js") ||
@@ -83,11 +81,9 @@ export const protocolIntegrationPicked = async (
} else if (domain === "zha") { } else if (domain === "zha") {
const entries = options?.config_entry const entries = options?.config_entry
? undefined ? undefined
: ( : await getConfigEntries(hass, {
await getConfigEntries(hass, { domain,
domain, });
})
).filter((e) => !e.disabled_by);
if ( if (
!isComponentLoaded(hass, "zha") || !isComponentLoaded(hass, "zha") ||
@@ -133,11 +129,9 @@ export const protocolIntegrationPicked = async (
} else if (domain === "matter") { } else if (domain === "matter") {
const entries = options?.config_entry const entries = options?.config_entry
? undefined ? undefined
: ( : await getConfigEntries(hass, {
await getConfigEntries(hass, { domain,
domain, });
})
).filter((e) => !e.disabled_by);
if ( if (
!isComponentLoaded(hass, domain) || !isComponentLoaded(hass, domain) ||
(!options?.config_entry && !entries?.length) (!options?.config_entry && !entries?.length)

View File

@@ -15,6 +15,7 @@ export type LocalizeKeys =
| `ui.card.weather.cardinal_direction.${string}` | `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}` | `ui.card.lawn_mower.actions.${string}`
| `ui.components.calendar.event.rrule.${string}` | `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}` | `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}` | `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}` | `ui.dialogs.more_info_control.lawn_mower.${string}`

View File

@@ -20,15 +20,6 @@ function findNestedItem(
}, obj); }, obj);
} }
function updateNestedItem(obj: any, path: ItemPath): any {
const lastKey = path.pop()!;
const parent = findNestedItem(obj, path);
parent[lastKey] = Array.isArray(parent[lastKey])
? [...parent[lastKey]]
: [parent[lastKey]];
return obj;
}
export function nestedArrayMove<A>( export function nestedArrayMove<A>(
obj: A, obj: A,
oldIndex: number, oldIndex: number,
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
oldPath?: ItemPath, oldPath?: ItemPath,
newPath?: ItemPath newPath?: ItemPath
): A { ): A {
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A; const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj; const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj; const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
if (!Array.isArray(from) || !Array.isArray(to)) {
return obj;
}
const item = from.splice(oldIndex, 1)[0]; const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item); to.splice(newIndex, 0, item);

View File

@@ -108,7 +108,6 @@ class HaDataTableLabels extends LitElement {
ha-label { ha-label {
--ha-label-background-color: var(--color, var(--grey-color)); --ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5; --ha-label-background-opacity: 0.5;
outline: 1px solid var(--outline-color);
} }
ha-button-menu { ha-button-menu {
border-radius: 10px; border-radius: 10px;

View File

@@ -204,29 +204,6 @@ export class HaDataTable extends LitElement {
this._checkedRowsChanged(); this._checkedRowsChanged();
} }
public select(ids: string[], clear?: boolean): void {
if (clear) {
this._checkedRows = [];
}
ids.forEach((id) => {
const row = this._filteredData.find((data) => data[this.id] === id);
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
this._checkedRows.push(id);
}
});
this._checkedRowsChanged();
}
public unselect(ids: string[]): void {
ids.forEach((id) => {
const index = this._checkedRows.indexOf(id);
if (index > -1) {
this._checkedRows.splice(index, 1);
}
});
this._checkedRowsChanged();
}
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this._filteredData.length) { if (this._filteredData.length) {
@@ -1034,7 +1011,6 @@ export class HaDataTable extends LitElement {
/* @noflip */ /* @noflip */
padding-inline-end: initial; padding-inline-end: initial;
width: 60px; width: 60px;
min-width: 60px;
} }
.mdc-data-table__table { .mdc-data-table__table {
@@ -1200,7 +1176,6 @@ export class HaDataTable extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
background-color: var(--primary-background-color);
} }
.group-header ha-icon-button { .group-header ha-icon-button {

View File

@@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement {
.daterangepicker select.hourselect, .daterangepicker select.hourselect,
.daterangepicker select.minuteselect, .daterangepicker select.minuteselect,
.daterangepicker select.secondselect { .daterangepicker select.secondselect {
background: var(--card-background-color); background: transparent;
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
color: var(--primary-color); color: var(--primary-color);
} }

View File

@@ -1,17 +1,5 @@
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css, import { customElement, property } from "lit/decorators";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import {
customElement,
property,
query,
state as litState,
} from "lit/decorators";
interface State { interface State {
bold: boolean; bold: boolean;
@@ -23,24 +11,11 @@ interface State {
} }
@customElement("ha-ansi-to-html") @customElement("ha-ansi-to-html")
export class HaAnsiToHtml extends LitElement { class HaAnsiToHtml extends LitElement {
@property() public content!: string; @property() public content!: string;
@query("pre") private _pre?: HTMLPreElement;
@litState() private _filter = "";
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`<pre></pre>`; return html`${this._parseTextToColoredPre(this.content)}`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
// handle initial content
if (this.content) {
this.parseTextToColoredPre(this.content);
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -49,7 +24,6 @@ export class HaAnsiToHtml extends LitElement {
overflow-x: auto; overflow-x: auto;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; overflow-wrap: break-word;
margin: 0;
} }
.bold { .bold {
font-weight: bold; font-weight: bold;
@@ -111,33 +85,11 @@ export class HaAnsiToHtml extends LitElement {
.bg-white { .bg-white {
background-color: rgb(204, 204, 204); background-color: rgb(204, 204, 204);
} }
::highlight(search-results) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`; `;
} }
/** private _parseTextToColoredPre(text) {
* add new lines to the log const pre = document.createElement("pre");
* @param lines log lines
* @param top should the new lines be added to the top of the log
*/
public parseLinesToColoredPre(lines: string[], top = false) {
for (const line of lines) {
this.parseLineToColoredPre(line, top);
}
}
/**
* Add a single line to the log
* @param line log line
* @param top should the new line be added to the top of the log
*/
public parseLineToColoredPre(line, top = false) {
const lineDiv = document.createElement("div");
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g; const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0; let i = 0;
@@ -151,7 +103,7 @@ export class HaAnsiToHtml extends LitElement {
backgroundColor: null, backgroundColor: null,
}; };
const addPart = (content) => { const addSpan = (content) => {
const span = document.createElement("span"); const span = document.createElement("span");
if (state.bold) { if (state.bold) {
span.classList.add("bold"); span.classList.add("bold");
@@ -172,18 +124,15 @@ export class HaAnsiToHtml extends LitElement {
span.classList.add(`bg-${state.backgroundColor}`); span.classList.add(`bg-${state.backgroundColor}`);
} }
span.appendChild(document.createTextNode(content)); span.appendChild(document.createTextNode(content));
lineDiv.appendChild(span); pre.appendChild(span);
}; };
/* eslint-disable no-cond-assign */ /* eslint-disable no-cond-assign */
let match; let match;
// eslint-disable-next-line // eslint-disable-next-line
while ((match = re.exec(line)) !== null) { while ((match = re.exec(text)) !== null) {
const j = match!.index; const j = match!.index;
const substring = line.substring(i, j); addSpan(text.substring(i, j));
if (substring) {
addPart(substring);
}
i = j + match[0].length; i = j + match[0].length;
if (match[1] === undefined) { if (match[1] === undefined) {
@@ -285,93 +234,9 @@ export class HaAnsiToHtml extends LitElement {
} }
}); });
} }
addSpan(text.substring(i));
const substring = line.substring(i); return pre;
if (substring) {
addPart(substring);
}
if (top) {
this._pre?.prepend(lineDiv);
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
} else {
this._pre?.appendChild(lineDiv);
}
// filter new lines if a filter is set
if (this._filter) {
this.filterLines(this._filter);
}
}
public parseTextToColoredPre(text) {
const lines = text.split("\n");
for (const line of lines) {
this.parseLineToColoredPre(line);
}
}
/**
* Filter lines based on a search string, lines and search string will be converted to lowercase
* @param filter the search string
* @returns true if there are lines to display
*/
filterLines(filter: string): boolean {
this._filter = filter;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
let numberOfFoundLines = 0;
if (!filter) {
lines.forEach((line) => {
line.style.display = "";
});
numberOfFoundLines = lines.length;
if (CSS.highlights) {
CSS.highlights.delete("search-results");
}
} else {
const highlightRanges: Range[] = [];
lines.forEach((line) => {
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
line.style.display = "none";
} else {
line.style.display = "";
numberOfFoundLines++;
if (CSS.highlights && line.firstChild !== null && line.textContent) {
const spansOfLine = line.querySelectorAll("span");
spansOfLine.forEach((span) => {
const text = span.textContent.toLowerCase();
const indices: number[] = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(filter.toLowerCase(), startPos);
if (index === -1) break;
indices.push(index);
startPos = index + filter.length;
}
indices.forEach((index) => {
const range = new Range();
range.setStart(span.firstChild!, index);
range.setEnd(span.firstChild!, index + filter.length);
highlightRanges.push(range);
});
});
}
}
});
if (CSS.highlights) {
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
}
}
return !!numberOfFoundLines;
}
public clear() {
if (this._pre) {
this._pre.innerHTML = "";
}
} }
} }

View File

@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
.card-header, .card-header,
:host ::slotted(.card-header) { :host ::slotted(.card-header) {
color: var(--ha-card-header-color, var(--primary-text-color)); color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit); font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em; letter-spacing: -0.012em;

View File

@@ -1,6 +1,6 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -8,9 +8,8 @@ import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeKeys } from "../common/translations/localize"; import { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-md-divider";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import "./ha-md-divider";
@customElement("ha-color-picker") @customElement("ha-color-picker")
export class HaColorPicker extends LitElement { export class HaColorPicker extends LitElement {
@@ -33,17 +32,7 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HaSelect; _valueSelected(ev) {
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value; const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value; this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@@ -52,13 +41,7 @@ export class HaColorPicker extends LitElement {
} }
render() { render() {
const value = this.value || this.defaultColor || ""; const value = this.value || this.defaultColor;
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html` return html`
<ha-select <ha-select
@@ -127,14 +110,6 @@ export class HaColorPicker extends LitElement {
</ha-list-item> </ha-list-item>
` `
)} )}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this.renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select> </ha-select>
`; `;
} }

View File

@@ -45,7 +45,7 @@ export class HaControlButton extends LitElement {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;

View File

@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
flex: none; flex: none;
} }
.mdc-top-app-bar__title { .mdc-top-app-bar__title {
padding-inline-start: 24px; padding-inline-start: 20px;
padding-inline-end: initial; padding-inline-end: initial;
} }
`, `,

View File

@@ -216,7 +216,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
ha-input-chip { ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color)); --md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5; --ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-outline-width: 1px;
} }
`; `;
} }

View File

@@ -86,11 +86,6 @@ export class HaMarkdown extends LitElement {
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`; `;
} }
} }

View File

@@ -118,7 +118,6 @@ export class HaPasswordField extends LitElement {
.type=${this._unmaskedPassword ? "text" : "password"} .type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`} .suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputChange} @input=${this._handleInputChange}
@change=${this._reDispatchEvent}
></ha-textfield> ></ha-textfield>
<ha-icon-button <ha-icon-button
toggles toggles
@@ -157,12 +156,6 @@ export class HaPasswordField extends LitElement {
this.value = ev.target.value; this.value = ev.target.value;
} }
@eventOptions({ passive: true })
private _reDispatchEvent(oldEvent: Event) {
const newEvent = new Event(oldEvent.type, oldEvent);
this.dispatchEvent(newEvent);
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;

View File

@@ -1,5 +1,4 @@
import { PropertyValues, ReactiveElement } from "lit"; import { PropertyValues, ReactiveElement } from "lit";
import { parseISO } from "date-fns";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time"; import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
@@ -59,12 +58,7 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) { if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never"); this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else { } else {
const date = const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this.hass.locale);
this.innerHTML = this.capitalize this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime) ? capitalizeFirstLetter(relTime)
: relTime; : relTime;

View File

@@ -34,7 +34,6 @@ import {
expandLabelTarget, expandLabelTarget,
Selector, Selector,
TargetSelector, TargetSelector,
TemplateSelector,
} from "../data/selector"; } from "../data/selector";
import { HomeAssistant, ValueChangedEvent } from "../types"; import { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url"; import { documentationUrl } from "../util/documentation-url";
@@ -46,7 +45,6 @@ import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon"; import "./ha-service-section-icon";
import { hasTemplate } from "../common/string/has-template";
const attributeFilter = (values: any[], attribute: any) => { const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") { if (typeof attribute === "object") {
@@ -63,11 +61,6 @@ const showOptionalToggle = (field) =>
!field.required && !field.required &&
!("boolean" in field.selector && field.default); !("boolean" in field.selector && field.default);
interface Field extends Omit<HassService["fields"][string], "selector"> {
key: string;
selector?: Selector;
}
interface ExtHassService extends Omit<HassService, "fields"> { interface ExtHassService extends Omit<HassService, "fields"> {
fields: Array< fields: Array<
Omit<HassService["fields"][string], "selector"> & { Omit<HassService["fields"][string], "selector"> & {
@@ -77,12 +70,9 @@ interface ExtHassService extends Omit<HassService, "fields"> {
collapsed?: boolean; collapsed?: boolean;
} }
>; >;
flatFields: Array<Field>;
hasSelector: string[]; hasSelector: string[];
} }
const TEMPLATE_SELECTOR: TemplateSelector = { template: {} };
@customElement("ha-service-control") @customElement("ha-service-control")
export class HaServiceControl extends LitElement { export class HaServiceControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -187,7 +177,7 @@ export class HaServiceControl extends LitElement {
if (!this._value.data) { if (!this._value.data) {
this._value.data = {}; this._value.data = {};
} }
serviceData.flatFields.forEach((field) => { serviceData.fields.forEach((field) => {
if ( if (
field.selector && field.selector &&
field.required && field.required &&
@@ -251,28 +241,22 @@ export class HaServiceControl extends LitElement {
selector: value.selector as Selector | undefined, selector: value.selector as Selector | undefined,
})); }));
const flatFields: Field[] = [];
const hasSelector: string[] = []; const hasSelector: string[] = [];
fields.forEach((field) => { fields.forEach((field) => {
if ((field as any).fields) { if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => { Object.entries((field as any).fields).forEach(([key, subField]) => {
flatFields.push({ ...(subField as Field), key });
if ((subField as any).selector) { if ((subField as any).selector) {
hasSelector.push(key); hasSelector.push(key);
} }
}); });
} else { } else if (field.selector) {
flatFields.push(field); hasSelector.push(field.key);
if (field.selector) {
hasSelector.push(field.key);
}
} }
}); });
return { return {
...serviceDomains[domain][serviceName], ...serviceDomains[domain][serviceName],
fields, fields,
flatFields,
hasSelector, hasSelector,
}; };
} }
@@ -413,7 +397,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean( const hasOptional = Boolean(
!shouldRenderServiceDataYaml && !shouldRenderServiceDataYaml &&
serviceData?.flatFields.some((field) => showOptionalToggle(field)) serviceData?.fields.some((field) => showOptionalToggle(field))
); );
const targetEntities = this._getTargetedEntities( const targetEntities = this._getTargetedEntities(
@@ -482,8 +466,7 @@ export class HaServiceControl extends LitElement {
>${this.hass.localize( >${this.hass.localize(
"ui.components.service-control.target_secondary" "ui.components.service-control.target_secondary"
)}</span )}</span
> ><ha-selector
<ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${this._targetSelector( .selector=${this._targetSelector(
serviceData.target as TargetSelector serviceData.target as TargetSelector
@@ -516,23 +499,8 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => { : serviceData?.fields.map((dataField) =>
if (!dataField.fields) { dataField.fields
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel ? html`<ha-expansion-panel
leftChevron leftChevron
.expanded=${!dataField.collapsed} .expanded=${!dataField.collapsed}
@@ -563,8 +531,14 @@ export class HaServiceControl extends LitElement {
) )
)} )}
</ha-expansion-panel>` </ha-expansion-panel>`
: nothing; : this._renderField(
})} `; dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
} }
private _getSectionDescription( private _getSectionDescription(
@@ -577,16 +551,6 @@ export class HaServiceControl extends LitElement {
); );
} }
private _hasFilteredFields(
dataFields: ExtHassService["fields"],
targetEntities: string[]
) {
return dataFields.some(
(dataField) =>
!dataField.filter || this._filterField(dataField.filter, targetEntities)
);
}
private _renderField = ( private _renderField = (
dataField: ExtHassService["fields"][number], dataField: ExtHassService["fields"][number],
hasOptional: boolean, hasOptional: boolean,
@@ -644,34 +608,23 @@ export class HaServiceControl extends LitElement {
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description` `component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span ) || dataField?.description}</span
> >
${hasTemplate(this._value?.data?.[dataField.key]) <ha-selector
? html` .disabled=${this.disabled ||
<ha-selector (showOptional &&
.selector=${TEMPLATE_SELECTOR} !this._checkedKeys.has(dataField.key) &&
.key=${dataField.key} (!this._value?.data ||
.hass=${this.hass} this._value.data[dataField.key] === undefined))}
.value=${this._value?.data?.[dataField.key]} .hass=${this.hass}
.disabled=${this.disabled} .selector=${enhancedSelector}
@value-changed=${this._serviceDataChanged} .key=${dataField.key}
></ha-selector> @value-changed=${this._serviceDataChanged}
` .value=${this._value?.data
: html` ? this._value.data[dataField.key]
<ha-selector : undefined}
.disabled=${this.disabled || .placeholder=${dataField.default}
(showOptional && .localizeValue=${this._localizeValueCallback}
!this._checkedKeys.has(dataField.key) && @item-moved=${this._itemMoved}
(!this._value?.data || ></ha-selector>
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data?.[dataField.key]}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
`}
</ha-settings-row>` </ha-settings-row>`
: ""; : "";
}; };
@@ -695,7 +648,7 @@ export class HaServiceControl extends LitElement {
const field = this._getServiceInfo( const field = this._getServiceInfo(
this._value?.action, this._value?.action,
this.hass.services this.hass.services
)?.flatFields.find((_field) => _field.key === key); )?.fields.find((_field) => _field.key === key);
let defaultValue = field?.default; let defaultValue = field?.default;

View File

@@ -42,17 +42,14 @@ export class HaSettingsRow extends LitElement {
padding-bottom: 8px; padding-bottom: 8px;
padding-left: 0; padding-left: 0;
padding-inline-start: 0; padding-inline-start: 0;
padding-right: 16px; padding-right: 16x;
padding-inline-end: 16px; padding-inline-end: 16px;
overflow: hidden; overflow: hidden;
display: var(--layout-vertical_-_display, flex); display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction, column); flex-direction: var(--layout-vertical_-_flex-direction);
justify-content: var( justify-content: var(--layout-center-justified_-_justify-content);
--layout-center-justified_-_justify-content, flex: var(--layout-flex_-_flex);
center flex-basis: var(--layout-flex_-_flex-basis);
);
flex: var(--layout-flex_-_flex, 1);
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
} }
.body[three-line] { .body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px); min-height: var(--paper-item-body-three-line-min-height, 88px);

View File

@@ -859,14 +859,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
font-weight: 400; font-weight: 400;
color: var( color: var(--sidebar-menu-button-text-color, --primary-text-color);
--sidebar-menu-button-text-color,
var(--primary-text-color)
);
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
background-color: var( background-color: var(
--sidebar-menu-button-background-color, --sidebar-menu-button-background-color,
var(--primary-background-color) --primary-background-color
); );
font-size: 20px; font-size: 20px;
align-items: center; align-items: center;

View File

@@ -1,49 +1,8 @@
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
import { styles } from "@material/mwc-snackbar/mwc-snackbar.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
@customElement("ha-toast") @customElement("ha-toast")
export class HaToast extends Snackbar { export class HaToast extends Snackbar {}
static override styles = [
styles,
css`
.mdc-snackbar--leading {
justify-content: center;
}
.mdc-snackbar {
margin: 8px;
right: calc(8px + env(safe-area-inset-right));
bottom: calc(8px + env(safe-area-inset-bottom));
left: calc(8px + env(safe-area-inset-left));
}
.mdc-snackbar__surface {
min-width: 350px;
max-width: 650px;
}
// Revert the default styles set by mwc-snackbar
@media (max-width: 480px), (max-width: 344px) {
.mdc-snackbar__surface {
min-width: inherit;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-snackbar {
right: env(safe-area-inset-right);
bottom: env(safe-area-inset-bottom);
left: env(safe-area-inset-left);
}
.mdc-snackbar__surface {
min-width: 100%;
}
}
`,
];
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -24,7 +24,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
); );
} }
.mdc-top-app-bar__title { .mdc-top-app-bar__title {
padding-inline-start: 24px; padding-inline-start: 20px;
padding-inline-end: initial; padding-inline-end: initial;
} }
`, `,

View File

@@ -321,7 +321,7 @@ export class TopAppBarBaseBase extends BaseElement {
overflow: auto; overflow: auto;
} }
.mdc-top-app-bar__title { .mdc-top-app-bar__title {
padding-inline-start: 24px; padding-inline-start: 20px;
padding-inline-end: initial; padding-inline-end: initial;
} }
`, `,

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-console */
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -8,17 +6,11 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
addWebRtcCandidate, import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
fetchWebRtcClientConfiguration,
WebRtcAnswer,
WebRTCClientConfiguration,
webRtcOffer,
WebRtcOfferEvent,
} from "../data/camera";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
@@ -31,7 +23,7 @@ import "./ha-alert";
class HaWebRtcPlayer extends LitElement { class HaWebRtcPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityid?: string; @property() public entityid!: string;
@property({ type: Boolean, attribute: "controls" }) @property({ type: Boolean, attribute: "controls" })
public controls = false; public controls = false;
@@ -45,24 +37,17 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" }) @property({ type: Boolean, attribute: "playsinline" })
public playsInline = false; public playsInline = false;
@property({ attribute: "poster-url" }) public posterUrl?: string; @property() public posterUrl!: string;
@state() private _error?: string; @state() private _error?: string;
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement; @query("#remote-stream") private _videoEl!: HTMLVideoElement;
private _clientConfig?: WebRTCClientConfiguration;
private _peerConnection?: RTCPeerConnection; private _peerConnection?: RTCPeerConnection;
private _remoteStream?: MediaStream; private _remoteStream?: MediaStream;
private _unsub?: Promise<UnsubscribeFunc>;
private _sessionId?: string;
private _candidatesList: string[] = [];
protected override render(): TemplateResult { protected override render(): TemplateResult {
if (this._error) { if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
@@ -74,7 +59,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted} .muted=${this.muted}
?playsinline=${this.playsInline} ?playsinline=${this.playsInline}
?controls=${this.controls} ?controls=${this.controls}
poster=${ifDefined(this.posterUrl)} .poster=${this.posterUrl}
@loadeddata=${this._loadedData} @loadeddata=${this._loadedData}
></video> ></video>
`; `;
@@ -82,7 +67,7 @@ class HaWebRtcPlayer extends LitElement {
public override connectedCallback() { public override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated && this.entityid) { if (this.hasUpdated) {
this._startWebRtc(); this._startWebRtc();
} }
} }
@@ -92,298 +77,121 @@ class HaWebRtcPlayer extends LitElement {
this._cleanUp(); this._cleanUp();
} }
protected override willUpdate(changedProperties: PropertyValues<this>) { protected override updated(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!changedProperties.has("entityid")) { if (!changedProperties.has("entityid")) {
return; return;
} }
if (!this._videoEl) {
return;
}
this._startWebRtc(); this._startWebRtc();
} }
private async _startWebRtc(): Promise<void> { private async _startWebRtc(): Promise<void> {
this._cleanUp();
if (!this.hass || !this.entityid) {
return;
}
console.time("WebRTC");
this._error = undefined; this._error = undefined;
console.timeLog("WebRTC", "start clientConfig"); const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
this._clientConfig = await fetchWebRtcClientConfiguration( // Some cameras (such as nest) require a data channel to establish a stream
this.hass, // however, not used by any integrations.
this.entityid peerConnection.createDataChannel("dataSendChannel");
); peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
this._peerConnection = new RTCPeerConnection(
this._clientConfig.configuration
);
if (this._clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
this._peerConnection.createDataChannel(this._clientConfig.dataChannel);
}
this._peerConnection.onnegotiationneeded = this._startNegotiation;
this._peerConnection.onicecandidate = this._handleIceCandidate;
this._peerConnection.oniceconnectionstatechange =
this._iceConnectionStateChanged;
// just for debugging
this._peerConnection.onsignalingstatechange = (ev) => {
switch ((ev.target as RTCPeerConnection).signalingState) {
case "stable":
console.timeLog("WebRTC", "ICE negotiation complete");
break;
default:
console.timeLog(
"WebRTC",
"Signaling state changed",
(ev.target as RTCPeerConnection).signalingState
);
}
};
// Setup callbacks to render remote stream once media tracks are discovered.
this._remoteStream = new MediaStream();
this._peerConnection.ontrack = this._addTrack;
this._peerConnection.addTransceiver("audio", { direction: "recvonly" });
this._peerConnection.addTransceiver("video", { direction: "recvonly" });
}
private _startNegotiation = async () => {
if (!this._peerConnection) {
return;
}
const offerOptions: RTCOfferOptions = { const offerOptions: RTCOfferOptions = {
offerToReceiveAudio: true, offerToReceiveAudio: true,
offerToReceiveVideo: true, offerToReceiveVideo: true,
}; };
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit = const offer: RTCSessionDescriptionInit =
await this._peerConnection.createOffer(offerOptions); await peerConnection.createOffer(offerOptions);
await peerConnection.setLocalDescription(offer);
if (!this._peerConnection) { let candidates = ""; // Build an Offer SDP string with ice candidates
return; const iceResolver = new Promise<void>((resolve) => {
} peerConnection.addEventListener("icecandidate", async (event) => {
if (!event.candidate?.candidate) {
console.timeLog("WebRTC", "end createOffer", offer); resolve(); // Gathering complete
return;
console.timeLog("WebRTC", "start setLocalDescription"); }
candidates += `a=${event.candidate.candidate}\r\n`;
await this._peerConnection.setLocalDescription(offer);
if (!this._peerConnection || !this.entityid) {
return;
}
console.timeLog("WebRTC", "end setLocalDescription");
let candidates = "";
if (this._clientConfig?.getCandidatesUpfront) {
await new Promise<void>((resolve) => {
this._peerConnection!.onicegatheringstatechange = (ev: Event) => {
const iceGatheringState = (ev.target as RTCPeerConnection)
.iceGatheringState;
if (iceGatheringState === "complete") {
this._peerConnection!.onicegatheringstatechange = null;
resolve();
}
console.timeLog(
"WebRTC",
"Ice gathering state changed",
iceGatheringState
);
};
}); });
});
if (!this._peerConnection || !this.entityid) { await iceResolver;
return;
}
}
while (this._candidatesList.length) {
const candidate = this._candidatesList.pop();
if (candidate) {
candidates += `a=${candidate}\r\n`;
}
}
const offer_sdp = offer.sdp! + candidates; const offer_sdp = offer.sdp! + candidates;
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp); let webRtcAnswer: WebRtcAnswer;
try { try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) => webRtcAnswer = await handleWebRtcOffer(
this._handleOfferEvent(event) this.hass,
this.entityid,
offer_sdp
); );
} catch (err: any) { } catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message; this._error = "Failed to start WebRTC stream: " + err.message;
this._cleanUp(); peerConnection.close();
}
};
private _iceConnectionStateChanged = () => {
console.timeLog(
"WebRTC",
"ice connection state change",
this._peerConnection?.iceConnectionState
);
if (this._peerConnection?.iceConnectionState === "failed") {
this._peerConnection.restartIce();
}
};
private async _handleOfferEvent(event: WebRtcOfferEvent) {
if (!this.entityid) {
return;
}
if (event.type === "session") {
this._sessionId = event.session_id;
this._candidatesList.forEach((candidate) =>
addWebRtcCandidate(
this.hass,
this.entityid!,
event.session_id,
candidate
)
);
this._candidatesList = [];
}
if (event.type === "answer") {
console.timeLog("WebRTC", "answer", event.answer);
this._handleAnswer(event);
}
if (event.type === "candidate") {
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
try {
await this._peerConnection?.addIceCandidate(
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
);
} catch (err: any) {
console.error(err);
}
}
if (event.type === "error") {
this._error = "Failed to start WebRTC stream: " + event.message;
this._cleanUp();
}
}
private _handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (!this.entityid || !event.candidate?.candidate) {
return; return;
} }
console.timeLog( // Setup callbacks to render remote stream once media tracks are discovered.
"WebRTC", const remoteStream = new MediaStream();
"local ice candidate", peerConnection.addEventListener("track", (event) => {
event.candidate?.candidate remoteStream.addTrack(event.track);
); this._videoEl.srcObject = remoteStream;
});
if (this._sessionId) { this._remoteStream = remoteStream;
addWebRtcCandidate(
this.hass,
this.entityid,
this._sessionId,
event.candidate?.candidate
);
} else {
this._candidatesList.push(event.candidate?.candidate);
}
};
private _addTrack = async (event: RTCTrackEvent) => {
if (!this._remoteStream) {
return;
}
this._remoteStream.addTrack(event.track);
if (!this.hasUpdated) {
await this.updateComplete;
}
this._videoEl.srcObject = this._remoteStream;
};
private async _handleAnswer(event: WebRtcAnswer) {
if (
!this._peerConnection?.signalingState ||
["stable", "closed"].includes(this._peerConnection.signalingState)
) {
return;
}
// Initiate the stream with the remote device // Initiate the stream with the remote device
const remoteDesc = new RTCSessionDescription({ const remoteDesc = new RTCSessionDescription({
type: "answer", type: "answer",
sdp: event.answer, sdp: webRtcAnswer.answer,
}); });
try { try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc); await peerConnection.setRemoteDescription(remoteDesc);
await this._peerConnection.setRemoteDescription(remoteDesc);
} catch (err: any) { } catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message; this._error = "Failed to connect WebRTC stream: " + err.message;
this._cleanUp(); peerConnection.close();
return;
} }
console.timeLog("WebRTC", "end setRemoteDescription"); this._peerConnection = peerConnection;
}
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
} }
private _cleanUp() { private _cleanUp() {
console.timeLog("WebRTC", "stopped");
console.timeEnd("WebRTC");
if (this._remoteStream) { if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => { this._remoteStream.getTracks().forEach((track) => {
track.stop(); track.stop();
}); });
this._remoteStream = undefined; this._remoteStream = undefined;
} }
const videoEl = this._videoEl; if (this._videoEl) {
if (videoEl) { this._videoEl.removeAttribute("src");
videoEl.removeAttribute("src"); this._videoEl.load();
videoEl.load();
} }
if (this._peerConnection) { if (this._peerConnection) {
this._peerConnection.close(); this._peerConnection.close();
this._peerConnection.onnegotiationneeded = null;
this._peerConnection.onicecandidate = null;
this._peerConnection.oniceconnectionstatechange = null;
this._peerConnection.onicegatheringstatechange = null;
this._peerConnection.ontrack = null;
// just for debugging
this._peerConnection.onsignalingstatechange = null;
this._peerConnection = undefined; this._peerConnection = undefined;
} }
this._unsub?.then((unsub) => unsub());
this._unsub = undefined;
this._sessionId = undefined;
this._candidatesList = [];
} }
private _loadedData() { private _loadedData() {
// @ts-ignore // @ts-ignore
fireEvent(this, "load"); fireEvent(this, "load");
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -43,7 +43,6 @@ class HaEntityMarker extends LitElement {
.marker { .marker {
display: flex; display: flex;
justify-content: center; justify-content: center;
text-align: center;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
width: 48px; width: 48px;

View File

@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time"; import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute"; import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { import { fullEntitiesContext, labelsContext } from "../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry"; import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry"; import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { import {
@@ -206,7 +201,6 @@ class ActionRenderer {
private hass: HomeAssistant, private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[], private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[], private labelReg: LabelRegistryEntry[],
private floorReg: { [id: string]: FloorRegistryEntry },
private entries: TemplateResult[], private entries: TemplateResult[],
private trace: AutomationTraceExtended, private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer, private logbookRenderer: LogbookRenderer,
@@ -325,7 +319,6 @@ class ActionRenderer {
this.hass, this.hass,
this.entityReg, this.entityReg,
this.labelReg, this.labelReg,
this.floorReg,
data, data,
actionType actionType
), ),
@@ -493,13 +486,7 @@ class ActionRenderer {
const name = const name =
repeatConfig.alias || repeatConfig.alias ||
describeAction( describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
this._renderEntry(repeatPath, name, undefined, disabled); this._renderEntry(repeatPath, name, undefined, disabled);
@@ -597,7 +584,6 @@ class ActionRenderer {
this.hass, this.hass,
this.entityReg, this.entityReg,
this.labelReg, this.labelReg,
this.floorReg,
sequenceConfig, sequenceConfig,
"sequence" "sequence"
), ),
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true }) @consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[]; _labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
protected render() { protected render() {
if (!this.trace) { if (!this.trace) {
return nothing; return nothing;
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg, this._labelReg,
this._floorReg,
entries, entries,
this.trace, this.trace,
logbookRenderer, logbookRenderer,

View File

@@ -8,7 +8,6 @@ import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES, migrateAutomationAction } from "./script"; import { Action, MODES, migrateAutomationAction } from "./script";
import { createSearchParam } from "../common/url/search-params";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
@@ -167,7 +166,7 @@ export interface TagTrigger extends BaseTrigger {
export interface TimeTrigger extends BaseTrigger { export interface TimeTrigger extends BaseTrigger {
trigger: "time"; trigger: "time";
at: string | { entity_id: string; offset?: string }; at: string;
} }
export interface TemplateTrigger extends BaseTrigger { export interface TemplateTrigger extends BaseTrigger {
@@ -463,13 +462,9 @@ export const flattenTriggers = (
return flatTriggers; return flatTriggers;
}; };
export const showAutomationEditor = ( export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
data?: Partial<AutomationConfig>,
expanded?: boolean
) => {
initialAutomationEditorData = data; initialAutomationEditorData = data;
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : ""; navigate("/config/automation/edit/new");
navigate(`/config/automation/edit/new${params}`);
}; };
export const duplicateAutomation = (config: AutomationConfig) => { export const duplicateAutomation = (config: AutomationConfig) => {

View File

@@ -8,7 +8,6 @@ import {
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation"; import { Condition, ForDict, Trigger } from "./automation";
import { import {
@@ -372,22 +371,13 @@ const tryDescribeTrigger = (
// Time Trigger // Time Trigger
if (trigger.trigger === "time" && trigger.at) { if (trigger.trigger === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) => { const result = ensureArray(trigger.at).map((at) =>
if (typeof at === "string") { typeof at !== "string"
if (isValidEntityId(at)) { ? at
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`; : at.includes(".")
} ? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
return localizeTimeString(at, hass.locale, hass.config); : localizeTimeString(at, hass.locale, hass.config)
} );
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
const offsetStr = at.offset
? " " +
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
offset: describeDuration(hass.locale, at.offset),
})
: "";
return `${entityStr}${offsetStr}`;
});
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: formatListWithOrs(hass.locale, result), time: formatListWithOrs(hass.locale, result),

View File

@@ -39,37 +39,10 @@ export interface Stream {
url: string; url: string;
} }
export type WebRtcOfferEvent =
| WebRtcId
| WebRtcAnswer
| WebRtcCandidate
| WebRtcError;
export interface WebRtcId {
type: "session";
session_id: string;
}
export interface WebRtcAnswer { export interface WebRtcAnswer {
type: "answer";
answer: string; answer: string;
} }
export interface WebRtcCandidate {
type: "candidate";
candidate: string;
}
export interface WebRtcError {
type: "error";
code: string;
message: string;
}
export interface WebRtcOfferResponse {
id: string;
}
export const cameraUrlWithWidthHeight = ( export const cameraUrlWithWidthHeight = (
base_url: string, base_url: string,
width: number, width: number,
@@ -121,29 +94,15 @@ export const fetchStreamUrl = async (
return stream; return stream;
}; };
export const webRtcOffer = ( export const handleWebRtcOffer = (
hass: HomeAssistant, hass: HomeAssistant,
entity_id: string, entityId: string,
offer: string, offer: string
callback: (event: WebRtcOfferEvent) => void
) => ) =>
hass.connection.subscribeMessage<WebRtcOfferEvent>(callback, { hass.callWS<WebRtcAnswer>({
type: "camera/webrtc/offer", type: "camera/web_rtc_offer",
entity_id, entity_id: entityId,
offer, offer: offer,
});
export const addWebRtcCandidate = (
hass: HomeAssistant,
entity_id: string,
session_id: string,
candidate: string
) =>
hass.callWS({
type: "camera/webrtc/candidate",
entity_id,
session_id,
candidate,
}); });
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) => export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
@@ -174,18 +133,3 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) => export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length); mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
getCandidatesUpfront: boolean;
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
type: "camera/webrtc/get_client_config",
entity_id: entityId,
});

View File

@@ -27,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext = export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities"); createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels"); export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -65,7 +65,7 @@ export const countryCurrency = {
HK: "HKD", HK: "HKD",
HN: "HNL", HN: "HNL",
HM: "AUD", HM: "AUD",
VE: "VED", VE: "VEF",
PR: "USD", PR: "USD",
PS: "ILS", PS: "ILS",
PW: "USD", PW: "USD",

View File

@@ -358,24 +358,21 @@ export const restartHassioAddon = async (
export const uninstallHassioAddon = async ( export const uninstallHassioAddon = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string, slug: string
removeData: boolean ) => {
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
type: "supervisor/api", type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`, endpoint: `/addons/${slug}/uninstall`,
method: "post", method: "post",
timeout: null, timeout: null,
data: { remove_config: removeData },
}); });
return; return;
} }
await hass.callApi<HassioResponse<void>>( await hass.callApi<HassioResponse<void>>(
"POST", "POST",
`hassio/addons/${slug}/uninstall`, `hassio/addons/${slug}/uninstall`
{ remove_config: removeData }
); );
}; };

View File

@@ -4,7 +4,7 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
interface IpConfiguration { interface IpConfiguration {
address: string[]; address: string[];
gateway: string | null; gateway: string;
method: "disabled" | "static" | "auto"; method: "disabled" | "static" | "auto";
nameservers: string[]; nameservers: string[];
} }
@@ -17,7 +17,7 @@ export interface NetworkInterface {
ipv4?: Partial<IpConfiguration>; ipv4?: Partial<IpConfiguration>;
ipv6?: Partial<IpConfiguration>; ipv6?: Partial<IpConfiguration>;
type: "ethernet" | "wireless" | "vlan"; type: "ethernet" | "wireless" | "vlan";
wifi?: Partial<WifiConfiguration> | null; wifi?: Partial<WifiConfiguration>;
} }
interface DockerNetwork { interface DockerNetwork {
@@ -27,7 +27,7 @@ interface DockerNetwork {
interface: string; interface: string;
} }
export interface AccessPoint { interface AccessPoint {
mode: "infrastructure" | "mesh" | "adhoc" | "ap"; mode: "infrastructure" | "mesh" | "adhoc" | "ap";
ssid: string; ssid: string;
mac: string; mac: string;
@@ -114,65 +114,3 @@ export const accesspointScan = async (
) )
); );
}; };
export const parseAddress = (address: string) => {
const [ip, cidr] = address.split("/");
return { ip, mask: cidrToNetmask(cidr, address.includes(":")) };
};
export const formatAddress = (ip: string, mask: string) =>
`${ip}/${netmaskToCidr(mask)}`;
// Helper functions
export const cidrToNetmask = (
cidr: string,
isIPv6: boolean = false
): string => {
const bits = parseInt(cidr, 10);
if (isIPv6) {
const fullMask = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
const numGroups = Math.floor(bits / 16);
const remainingBits = bits % 16;
const lastGroup = remainingBits
? parseInt(
"1".repeat(remainingBits) + "0".repeat(16 - remainingBits),
2
).toString(16)
: "";
return fullMask
.split(":")
.slice(0, numGroups)
.concat(lastGroup)
.concat(Array(8 - numGroups - (lastGroup ? 1 : 0)).fill("0"))
.join(":");
}
/* eslint-disable no-bitwise */
const mask = ~(2 ** (32 - bits) - 1);
return [
(mask >>> 24) & 255,
(mask >>> 16) & 255,
(mask >>> 8) & 255,
mask & 255,
].join(".");
/* eslint-enable no-bitwise */
};
export const netmaskToCidr = (netmask: string): number => {
if (netmask.includes(":")) {
// IPv6
return netmask
.split(":")
.map((group) =>
group ? (parseInt(group, 16).toString(2).match(/1/g) || []).length : 0
)
.reduce((sum, val) => sum + val, 0);
}
// IPv4
return netmask
.split(".")
.reduce(
(count, octet) =>
count + (parseInt(octet, 10).toString(2).match(/1/g) || []).length,
0
);
};

View File

@@ -65,10 +65,6 @@ export type HassioInfo = {
timezone: string; timezone: string;
}; };
export type HassioBoots = {
boots: Record<number, string>;
};
export type HassioPanelInfo = PanelInfo< export type HassioPanelInfo = PanelInfo<
| undefined | undefined
| { | {
@@ -181,39 +177,10 @@ export const fetchHassioInfo = async (
); );
}; };
export const fetchHassioBoots = async (hass: HomeAssistant) => export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`); hass.callApi<string>(
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string,
range?: string,
boot = 0
) =>
hass.callApiRaw(
"GET", "GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`, `hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
undefined,
range
? {
Range: range,
}
: undefined
);
export const fetchHassioLogsFollow = async (
hass: HomeAssistant,
provider: string,
signal: AbortSignal,
lines = 100,
boot = 0
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
undefined,
undefined,
signal
); );
export const getHassioLogDownloadUrl = (provider: string) => export const getHassioLogDownloadUrl = (provider: string) =>
@@ -221,15 +188,6 @@ export const getHassioLogDownloadUrl = (provider: string) =>
provider.includes("_") ? `addons/${provider}` : provider provider.includes("_") ? `addons/${provider}` : provider
}/logs`; }/logs`;
export const getHassioLogDownloadLinesUrl = (
provider: string,
lines: number,
boot = 0
) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs/boots/${boot}?lines=${lines}`;
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,
data: SupervisorOptions data: SupervisorOptions

View File

@@ -22,7 +22,6 @@ export type IntegrationType =
export interface IntegrationManifest { export interface IntegrationManifest {
is_built_in: boolean; is_built_in: boolean;
overwrites_built_in?: boolean;
domain: string; domain: string;
name: string; name: string;
config_flow: boolean; config_flow: boolean;

View File

@@ -11,7 +11,6 @@ export interface Integration {
iot_class?: string; iot_class?: string;
supported_by?: string; supported_by?: string;
is_built_in?: boolean; is_built_in?: boolean;
overwrites_built_in?: boolean;
single_config_entry?: boolean; single_config_entry?: boolean;
} }
@@ -24,7 +23,6 @@ export interface Brand {
integrations?: Integrations; integrations?: Integrations;
iot_standards?: IotStandards[]; iot_standards?: IotStandards[];
is_built_in?: boolean; is_built_in?: boolean;
overwrites_built_in?: boolean;
} }
export interface Brands { export interface Brands {

View File

@@ -50,23 +50,14 @@ export interface LogbookEntry {
// Localization mapping for all the triggers in core // Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers // in homeassistant.components.homeassistant.triggers
// //
type TriggerPhraseKeys = const triggerPhrases = {
| "triggered_by_numeric_state_of" "numeric state of": "triggered_by_numeric_state_of", // number state trigger
| "triggered_by_state_of" "state of": "triggered_by_state_of", // state trigger
| "triggered_by_event" event: "triggered_by_event", // event trigger
| "triggered_by_time" time: "triggered_by_time", // time trigger
| "triggered_by_time_pattern" "time pattern": "triggered_by_time_pattern", // time trigger
| "triggered_by_homeassistant_stopping" "Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
| "triggered_by_homeassistant_starting"; "Home Assistant starting": "triggered_by_homeassistant_starting", // start event
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
triggered_by_numeric_state_of: "numeric state of", // number state trigger
triggered_by_state_of: "state of", // state trigger
triggered_by_event: "event", // event trigger
triggered_by_time: "time", // time trigger
triggered_by_time_pattern: "time pattern", // time trigger
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
}; };
export const getLogbookDataForContext = async ( export const getLogbookDataForContext = async (
@@ -176,14 +167,11 @@ export const localizeTriggerSource = (
localize: LocalizeFunc, localize: LocalizeFunc,
source: string source: string
) => { ) => {
for (const triggerPhraseKey of Object.keys( for (const triggerPhrase in triggerPhrases) {
triggerPhrases if (source.startsWith(triggerPhrase)) {
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
if (source.startsWith(phrase)) {
return source.replace( return source.replace(
phrase, triggerPhrase,
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}` `${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
); );
} }
} }

View File

@@ -28,7 +28,6 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig { export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info"; action: "more-info";
entity_id?: string;
} }
export interface AssistActionConfig extends BaseActionConfig { export interface AssistActionConfig extends BaseActionConfig {

View File

@@ -332,6 +332,3 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean => export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":"); statisticsId.includes(":");
export const updateStatisticsIssues = (hass: HomeAssistant) =>
hass.callWS({ type: "recorder/update_statistics_issues" });

View File

@@ -0,0 +1,10 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});

View File

@@ -28,7 +28,6 @@ import {
} from "./automation"; } from "./automation";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id"; import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
export const MODES = ["single", "restart", "queued", "parallel"] as const; export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const; export const MODES_MAX = ["queued", "parallel"] as const;
@@ -348,13 +347,9 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
entity_id, entity_id,
}); });
export const showScriptEditor = ( export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
data?: Partial<ScriptConfig>,
expanded?: boolean
) => {
inititialScriptEditorData = data; inititialScriptEditorData = data;
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : ""; navigate("/config/script/edit/new");
navigate(`/config/script/edit/new${params}`);
}; };
export const getScriptEditorInitData = () => { export const getScriptEditorInitData = () => {

View File

@@ -14,7 +14,6 @@ import {
computeEntityRegistryName, computeEntityRegistryName,
entityRegistryById, entityRegistryById,
} from "./entity_registry"; } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration"; import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry"; import { LabelRegistryEntry } from "./label_registry";
import { import {
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[], labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
hass, hass,
entityRegistry, entityRegistry,
labelRegistry, labelRegistry,
floorRegistry,
action, action,
actionType, actionType,
ignoreAlias ignoreAlias
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[], labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -168,7 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
); );
} }
} else if (key === "floor_id") { } else if (key === "floor_id") {
const floor = floorRegistry[targetThing] ?? undefined; const floor = hass.floors[targetThing] ?? undefined;
if (floor?.name) { if (floor?.name) {
targets.push(floor.name); targets.push(floor.name);
} else { } else {

View File

@@ -1,7 +1,6 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface ThreadRouter { export interface ThreadRouter {
instance_name: string;
addresses: [string]; addresses: [string];
border_agent_id: string | null; border_agent_id: string | null;
brand: "google" | "apple" | "homeassistant"; brand: "google" | "apple" | "homeassistant";
@@ -19,7 +18,7 @@ export interface ThreadDataSet {
channel: number | null; channel: number | null;
created: string; created: string;
dataset_id: string; dataset_id: string;
extended_pan_id: string; extended_pan_id: string | null;
network_name: string; network_name: string;
pan_id: string | null; pan_id: string | null;
preferred_border_agent_id: string | null; preferred_border_agent_id: string | null;

View File

@@ -72,8 +72,8 @@ export const timerTimeRemaining = (
if (stateObj.state === "active") { if (stateObj.state === "active") {
const now = new Date().getTime(); const now = new Date().getTime();
const finishes = new Date(stateObj.attributes.finishes_at).getTime(); const madeActive = new Date(stateObj.last_changed).getTime();
timeRemaining = Math.max((finishes - now) / 1000, 0); timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
} }
return timeRemaining; return timeRemaining;

View File

@@ -8,7 +8,6 @@ import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number";
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";
@@ -24,15 +23,13 @@ export enum UpdateEntityFeature {
interface UpdateEntityAttributes extends HassEntityAttributeBase { interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null; auto_update: boolean | null;
display_precision: number;
installed_version: string | null; installed_version: string | null;
in_progress: boolean; in_progress: boolean | number;
latest_version: string | null; latest_version: string | null;
release_summary: string | null; release_summary: string | null;
release_url: string | null; release_url: string | null;
skipped_version: string | null; skipped_version: string | null;
title: string | null; title: string | null;
update_percentage: number | null;
} }
export interface UpdateEntity extends HassEntityBase { export interface UpdateEntity extends HassEntityBase {
@@ -41,7 +38,7 @@ export interface UpdateEntity extends HassEntityBase {
export const updateUsesProgress = (entity: UpdateEntity): boolean => export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) && supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
entity.attributes.update_percentage !== null; typeof entity.attributes.in_progress === "number";
export const updateCanInstall = ( export const updateCanInstall = (
entity: UpdateEntity, entity: UpdateEntity,
@@ -52,7 +49,7 @@ export const updateCanInstall = (
supportsFeature(entity, UpdateEntityFeature.INSTALL); supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean => export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress; updateUsesProgress(entity) || !!entity.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>({
@@ -186,13 +183,10 @@ export const computeUpdateStateDisplay = (
if (updateIsInstalling(stateObj)) { if (updateIsInstalling(stateObj)) {
const supportsProgress = const supportsProgress =
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) && supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
attributes.update_percentage !== null; typeof attributes.in_progress === "number";
if (supportsProgress) { if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", { return hass.localize("ui.card.update.installing_with_progress", {
progress: formatNumber(attributes.update_percentage!, hass.locale, { progress: attributes.in_progress as number,
maximumFractionDigits: attributes.display_precision,
minimumFractionDigits: attributes.display_precision,
}),
}); });
} }
return hass.localize("ui.card.update.installing"); return hass.localize("ui.card.update.installing");

View File

@@ -9,15 +9,23 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { import {
DataEntryFlowStep, DataEntryFlowStep,
subscribeDataEntryFlowProgressed, subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow"; } from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
@@ -54,7 +62,7 @@ declare global {
@customElement("dialog-data-entry-flow") @customElement("dialog-data-entry-flow")
class DataEntryFlowDialog extends LitElement { class DataEntryFlowDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; public hass!: HomeAssistant;
@state() private _params?: DataEntryFlowDialogParams; @state() private _params?: DataEntryFlowDialogParams;
@@ -68,8 +76,16 @@ class DataEntryFlowDialog extends LitElement {
// Null means we need to pick a config flow // Null means we need to pick a config flow
| null; | null;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _handler?: string; @state() private _handler?: string;
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>; private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> { public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
@@ -167,7 +183,16 @@ class DataEntryFlowDialog extends LitElement {
this._loading = undefined; this._loading = undefined;
this._step = undefined; this._step = undefined;
this._params = undefined; this._params = undefined;
this._devices = undefined;
this._handler = undefined; this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
if (this._unsubDataEntryFlowProgressed) { if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => { this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub(); unsub();
@@ -284,13 +309,25 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass} .hass=${this.hass}
></step-flow-menu> ></step-flow-menu>
` `
: html` : this._devices === undefined ||
<step-flow-create-entry this._areas === undefined
.flowConfig=${this._params.flowConfig} ? // When it's a create entry result, we will fetch device & area registry
.step=${this._step} html`
.hass=${this.hass} <step-flow-loading
></step-flow-create-entry> .flowConfig=${this._params.flowConfig}
`} .hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
`} `}
</div> </div>
</ha-dialog> </ha-dialog>
@@ -314,6 +351,32 @@ class DataEntryFlowDialog extends LitElement {
// external and progress step will send update event from the backend, so we should subscribe to them // external and progress step will send update event from the backend, so we should subscribe to them
this._subscribeDataEntryFlowProgressed(); this._subscribeDataEntryFlowProgressed();
} }
if (this._step.type === "create_entry") {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {
this._devices = [];
this._areas = [];
}
}
}
private async _fetchDevices(configEntryId) {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
);
}
private async _fetchAreas() {
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
} }
private async _processStep( private async _processStep(

View File

@@ -20,7 +20,7 @@ export const showConfigFlowDialog = (
): void => ): void =>
showFlowDialog(element, dialogParams, { showFlowDialog(element, dialogParams, {
flowType: "config_flow", flowType: "config_flow",
showDevices: true, loadDevicesAndAreas: true,
createFlow: async (hass, handler) => { createFlow: async (hass, handler) => {
const [step] = await Promise.all([ const [step] = await Promise.all([
createConfigFlow(hass, handler, dialogParams.entryId), createConfigFlow(hass, handler, dialogParams.entryId),

View File

@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../types";
export interface FlowConfig { export interface FlowConfig {
flowType: FlowType; flowType: FlowType;
showDevices: boolean; loadDevicesAndAreas: boolean;
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>; createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
@@ -134,7 +134,8 @@ export interface FlowConfig {
export type LoadingReason = export type LoadingReason =
| "loading_handlers" | "loading_handlers"
| "loading_flow" | "loading_flow"
| "loading_step"; | "loading_step"
| "loading_devices_areas";
export interface DataEntryFlowDialogParams { export interface DataEntryFlowDialogParams {
startFlowHandler?: string; startFlowHandler?: string;

View File

@@ -29,7 +29,7 @@ export const showOptionsFlowDialog = (
}, },
{ {
flowType: "options_flow", flowType: "options_flow",
showDevices: false, loadDevicesAndAreas: false,
createFlow: async (hass, handler) => { createFlow: async (hass, handler) => {
const [step] = await Promise.all([ const [step] = await Promise.all([
createOptionsFlow(hass, handler), createOptionsFlow(hass, handler),

View File

@@ -4,7 +4,6 @@ import {
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
@@ -35,16 +34,7 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
private _devices = memoizeOne( @property({ attribute: false }) public devices!: DeviceRegistryEntry[];
(
showDevices: boolean,
devices: DeviceRegistryEntry[],
entry_id?: string
) =>
showDevices && entry_id
? devices.filter((device) => device.config_entries.includes(entry_id))
: []
);
private _deviceEntities = memoizeOne( private _deviceEntities = memoizeOne(
( (
@@ -60,48 +50,35 @@ class StepFlowCreateEntry extends LitElement {
); );
protected willUpdate(changedProps: PropertyValues) { protected willUpdate(changedProps: PropertyValues) {
if (!changedProps.has("devices") && !changedProps.has("hass")) {
return;
}
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
if ( if (
devices.length !== 1 || (changedProps.has("devices") || changedProps.has("hass")) &&
devices[0].primary_config_entry !== this.step.result?.entry_id this.devices.length === 1
) { ) {
return; // integration_type === "device"
} const assistSatellites = this._deviceEntities(
this.devices[0].id,
const assistSatellites = this._deviceEntities( Object.values(this.hass.entities),
devices[0].id, "assist_satellite"
Object.values(this.hass.entities), );
"assist_satellite" if (
); assistSatellites.length &&
if ( assistSatellites.some((satellite) =>
assistSatellites.length && assistSatelliteSupportsSetupFlow(
assistSatellites.some((satellite) => this.hass.states[satellite.entity_id]
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id]) )
) )
) { ) {
this._flowDone(); this._flowDone();
showVoiceAssistantSetupDialog(this, { showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id, deviceId: this.devices[0].id,
}); });
}
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const localize = this.hass.localize; const localize = this.hass.localize;
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
return html` return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2> <h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<div class="content"> <div class="content">
@@ -112,9 +89,9 @@ class StepFlowCreateEntry extends LitElement {
"ui.panel.config.integrations.config_flow.not_loaded" "ui.panel.config.integrations.config_flow.not_loaded"
)}</span )}</span
>` >`
: nothing} : ""}
${devices.length === 0 ${this.devices.length === 0
? nothing ? ""
: html` : html`
<p> <p>
${localize( ${localize(
@@ -122,7 +99,7 @@ class StepFlowCreateEntry extends LitElement {
)}: )}:
</p> </p>
<div class="devices"> <div class="devices">
${devices.map( ${this.devices.map(
(device) => html` (device) => html`
<div class="device"> <div class="device">
<div> <div>

View File

@@ -31,9 +31,6 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"valve", "valve",
"water_heater", "water_heater",
]; ];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
/** Domains with separate more info dialog. */ /** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [ export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel", "alarm_control_panel",

View File

@@ -93,13 +93,12 @@ class MoreInfoCover extends LitElement {
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) || supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT); supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
const supportsOpenCloseOnly = const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) && supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) && supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) && !supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
!supportsTilt && !supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
!supportsPosition && !supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
!supportsTiltPosition;
return html` return html`
<ha-more-info-state-header <ha-more-info-state-header
@@ -134,7 +133,7 @@ class MoreInfoCover extends LitElement {
${ ${
this._mode === "button" this._mode === "button"
? html` ? html`
${supportsOpenCloseOnly ${supportsOpenCloseWithoutStop
? html` ? html`
<ha-state-control-cover-toggle <ha-state-control-cover-toggle
.stateObj=${this.stateObj} .stateObj=${this.stateObj}

View File

@@ -1,18 +1,15 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress"; import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const"; import { BINARY_STATE_OFF } from "../../../common/const";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-faded"; import "../../../components/ha-faded";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { import {
UpdateEntity, UpdateEntity,
@@ -21,7 +18,6 @@ import {
updateReleaseNotes, updateReleaseNotes,
} from "../../../data/update"; } from "../../../data/update";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@customElement("more-info-update") @customElement("more-info-update")
class MoreInfoUpdate extends LitElement { class MoreInfoUpdate extends LitElement {
@@ -33,8 +29,6 @@ class MoreInfoUpdate extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _markdownLoading = true;
protected render() { protected render() {
if ( if (
!this.hass || !this.hass ||
@@ -50,117 +44,102 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.latest_version; this.stateObj.attributes.latest_version;
return html` return html`
<div class="content"> ${this.stateObj.attributes.in_progress
${this.stateObj.attributes.in_progress ? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) && typeof this.stateObj.attributes.in_progress === "number"
this.stateObj.attributes.update_percentage !== null ? html`<mwc-linear-progress
? html`<mwc-linear-progress .progress=${this.stateObj.attributes.in_progress / 100}
.progress=${this.stateObj.attributes.update_percentage / 100} buffer=""
buffer="" ></mwc-linear-progress>`
></mwc-linear-progress>` : html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>` : ""}
: nothing} <h3>${this.stateObj.attributes.title}</h3>
<h3>${this.stateObj.attributes.title}</h3> ${this._error
${this._error ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` : ""}
: nothing} <div class="row">
<div class="row"> <div class="key">
<div class="key"> ${this.hass.formatEntityAttributeName(
${this.hass.formatEntityAttributeName( this.stateObj,
this.stateObj, "installed_version"
"installed_version" )}
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div> </div>
<div class="row"> <div class="value">
<div class="key"> ${this.stateObj.attributes.installed_version ??
${this.hass.formatEntityAttributeName( this.hass.localize("state.default.unavailable")}
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div> </div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: nothing}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`
<hr />
${this._markdownLoading ? this._renderLoader() : nothing}
`
: html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this.stateObj.attributes.release_summary
? html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this.stateObj.attributes.release_summary}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing}
</div> </div>
<div class="footer"> <div class="row">
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP) <div class="key">
? html` ${this.hass.formatEntityAttributeName(
<ha-settings-row> this.stateObj,
<span slot="heading"> "latest_version"
${this.hass.localize( )}
"ui.dialogs.more_info_control.update.create_backup" </div>
)} <div class="value">
</span> ${this.stateObj.attributes.latest_version ??
<ha-switch this.hass.localize("state.default.unavailable")}
id="create_backup" </div>
checked </div>
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch> ${this.stateObj.attributes.release_url
</ha-settings-row> ? html`<div class="row">
` <div class="key">
: nothing} <a
<div class="actions"> href=${this.stateObj.attributes.release_url}
${this.stateObj.state === BINARY_STATE_OFF && target="_blank"
this.stateObj.attributes.skipped_version rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: ""}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`<div class="flex center">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: html`<hr />
<ha-faded>
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
</ha-faded> `
: this.stateObj.attributes.release_summary
? html`<hr />
<ha-markdown
.content=${this.stateObj.attributes.release_summary}
></ha-markdown>`
: ""}
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`<hr />
<ha-formfield
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
>
<ha-checkbox
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-checkbox>
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.attributes.auto_update
? ""
: this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html` ? html`
<ha-button @click=${this._handleClearSkipped}> <mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped" "ui.dialogs.more_info_control.update.clear_skipped"
)} )}
</ha-button> </mwc-button>
` `
: html` : html`
<ha-button <mwc-button
@click=${this._handleSkip} @click=${this._handleSkip}
.disabled=${skippedVersion || .disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF || this.stateObj.state === BINARY_STATE_OFF ||
@@ -169,55 +148,35 @@ class MoreInfoUpdate extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.update.skip" "ui.dialogs.more_info_control.update.skip"
)} )}
</ha-button> </mwc-button>
`} `}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL) ${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html` ? html`
<ha-button <mwc-button
@click=${this._handleInstall} @click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF && .disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) || !skippedVersion) ||
updateIsInstalling(this.stateObj)} updateIsInstalling(this.stateObj)}
> >
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.update.update" "ui.dialogs.more_info_control.update.install"
)} )}
</ha-button> </mwc-button>
` `
: nothing} : ""}
</div>
</div>
`;
}
private _renderLoader() {
return html`
<div class="flex center loader">
<ha-circular-progress indeterminate></ha-circular-progress>
</div> </div>
`; `;
} }
protected firstUpdated(): void { protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) { if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
this._fetchReleaseNotes(); updateReleaseNotes(this.hass, this.stateObj!.entity_id)
} .then((result) => {
} this._releaseNotes = result;
})
private async _markdownLoaded() { .catch((err) => {
if (this._markdownLoading) { this._error = err.message;
this._markdownLoading = false; });
}
}
private async _fetchReleaseNotes() {
try {
this._releaseNotes = await updateReleaseNotes(
this.hass,
this.stateObj!.entity_id
);
} catch (err: any) {
this._error = err.message;
} }
} }
@@ -225,11 +184,9 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) { if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null; return null;
} }
const createBackupSwitch = this.shadowRoot?.getElementById( const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
"create-backup" if (checkbox) {
) as HaSwitch; return checkbox.checked;
if (createBackupSwitch) {
return createBackupSwitch.checked;
} }
return true; return true;
} }
@@ -254,17 +211,6 @@ class MoreInfoUpdate extends LitElement {
} }
private _handleSkip(): void { private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
),
text: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
),
});
return;
}
this.hass.callService("update", "skip", { this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id, entity_id: this.stateObj!.entity_id,
}); });
@@ -278,12 +224,6 @@ class MoreInfoUpdate extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
hr { hr {
border-color: var(--divider-color); border-color: var(--divider-color);
border-bottom: none; border-bottom: none;
@@ -298,44 +238,26 @@ class MoreInfoUpdate extends LitElement {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
.actions {
.footer {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
background: var( background: var(
--ha-dialog-surface-background, --ha-dialog-surface-background,
var(--mdc-theme-surface, #fff) var(--mdc-theme-surface, #fff)
); );
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
position: sticky; position: sticky;
bottom: 0; bottom: 0;
margin: 0 -24px -24px -24px; padding: 12px 0;
box-sizing: border-box; margin-bottom: -24px;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 10;
}
ha-settings-row {
width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
}
.actions {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
box-sizing: border-box;
padding: 12px;
z-index: 1; z-index: 1;
gap: 8px;
} }
.actions mwc-button {
margin: 0 4px 4px;
}
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
@@ -350,16 +272,6 @@ class MoreInfoUpdate extends LitElement {
} }
ha-markdown { ha-markdown {
direction: ltr; direction: ltr;
padding-bottom: 16px;
box-sizing: border-box;
}
ha-markdown.hidden {
display: none;
}
.loader {
height: 80px;
box-sizing: border-box;
padding-bottom: 16px;
} }
`; `;
} }

View File

@@ -83,11 +83,10 @@ class MoreInfoValve extends LitElement {
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) || supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
supportsFeature(this.stateObj, ValveEntityFeature.STOP); supportsFeature(this.stateObj, ValveEntityFeature.STOP);
const supportsOpenCloseOnly = const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) && supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) && supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, ValveEntityFeature.STOP) && !supportsFeature(this.stateObj, ValveEntityFeature.STOP);
!supportsPosition;
return html` return html`
<ha-more-info-state-header <ha-more-info-state-header
@@ -114,7 +113,7 @@ class MoreInfoValve extends LitElement {
${ ${
this._mode === "button" this._mode === "button"
? html` ? html`
${supportsOpenCloseOnly ${supportsOpenCloseWithoutStop
? html` ? html`
<ha-state-control-valve-toggle <ha-state-control-valve-toggle
.stateObj=${this.stateObj} .stateObj=${this.stateObj}

View File

@@ -9,7 +9,6 @@ import {
computeShowHistoryComponent, computeShowHistoryComponent,
computeShowLogBookComponent, computeShowLogBookComponent,
computeShowNewMoreInfo, computeShowNewMoreInfo,
DOMAINS_FULL_HEIGHT_MORE_INFO,
DOMAINS_NO_INFO, DOMAINS_NO_INFO,
DOMAINS_WITH_MORE_INFO, DOMAINS_WITH_MORE_INFO,
} from "./const"; } from "./const";
@@ -41,8 +40,6 @@ export class MoreInfoInfo extends LitElement {
const entityRegObj = this.hass.entities[entityId]; const entityRegObj = this.hass.entities[entityId];
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj); const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
const isFullHeight =
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
return html` return html`
<div class="container" data-domain=${domain}> <div class="container" data-domain=${domain}>
@@ -92,7 +89,7 @@ export class MoreInfoInfo extends LitElement {
.entityId=${this.entityId} .entityId=${this.entityId}
></ha-more-info-logbook>`} ></ha-more-info-logbook>`}
<more-info-content <more-info-content
?full-height=${isFullHeight} ?full-height=${isNewMoreInfo}
.stateObj=${stateObj} .stateObj=${stateObj}
.hass=${this.hass} .hass=${this.hass}
.entry=${this.entry} .entry=${this.entry}

View File

@@ -51,7 +51,6 @@ export class HuiPersistentNotificationItem extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.time { .time {
position: relative;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 6px; margin-top: 6px;

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