mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-23 16:51:49 +00:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b24eb2c916 | |||
| 071f8e96ae | |||
| 734fed21a8 | |||
| af203d640f | |||
| da29c8f536 | |||
| 8069596c87 | |||
| ace55fdb92 | |||
| dae8adab98 | |||
| 1522d979de | |||
| 5fd253b2d3 | |||
| f7d5195161 | |||
| b72791a9e2 | |||
| a3c0e8d519 | |||
| 19fcb9d2f7 | |||
| cbd90884ee | |||
| fce1938f38 | |||
| 24821d6f1b | |||
| 677d64c915 | |||
| fe06772a73 | |||
| 170f8c371a | |||
| 12841b5ff7 | |||
| 5393b05636 | |||
| a384e2dbd6 | |||
| fd4936e547 | |||
| 44d02420ae | |||
| ebf80ecca0 | |||
| bcfcc7bd5a | |||
| f7933c31d7 | |||
| 3f4f4a5ead | |||
| 44a269b87b | |||
| 4f89056883 | |||
| 6a40f1965a | |||
| 7e836d6cca | |||
| 1fab54831f | |||
| 4638582c6f | |||
| e5721fb134 | |||
| bfd8cb54c9 | |||
| 89bd1058df | |||
| 405727502f | |||
| dae105531f | |||
| 5f790a4977 | |||
| d6c16e0736 | |||
| c562f58326 | |||
| ce5640d13a | |||
| 6ddcc83638 | |||
| 396f495c9b | |||
| d994fd8928 | |||
| 21d8fda76d | |||
| 49716f4151 | |||
| 657bef6a75 | |||
| 9edd330728 | |||
| 09e83b6450 | |||
| 9c3f3ed05d | |||
| aec6c8c1e4 | |||
| 82f4ae1f08 | |||
| 2809091b44 | |||
| b2dda0f739 | |||
| d64845f206 | |||
| 44d929bf56 | |||
| 56cfff6922 | |||
| be8782d928 | |||
| 2eba8425a7 | |||
| 5ddc26df7a | |||
| 97516f5625 | |||
| e8c06b4220 | |||
| 4fd976dc8c | |||
| f8d8dc4eaa | |||
| 8ccda740ee | |||
| 8528dd8a15 | |||
| ac2f8ebce3 | |||
| 1462f65f5a | |||
| 3e9d3d90a1 | |||
| f28898551b | |||
| eabbcf3a95 | |||
| 01255cebc6 | |||
| d20e062de9 | |||
| 835c0fa35c | |||
| e308272d89 | |||
| d2ae376058 | |||
| fdd57645ee | |||
| 1ef1655a4c | |||
| aa1108fc41 | |||
| e3c6a57080 | |||
| 1e22649ef8 | |||
| 5abd04d09a | |||
| a5bf35690b | |||
| d98eb47490 | |||
| 738e92d27d | |||
| ade2e9272b | |||
| d8ce60dfb6 | |||
| db9374925e | |||
| 1bcd1293c0 | |||
| b8cf061ebb | |||
| 6585da9a73 | |||
| 368df82e97 | |||
| 1d99a5dff9 | |||
| 0ca72b763a | |||
| 31848a1efd | |||
| c6f79c2093 | |||
| 1a5ab1903a | |||
| a410a53524 | |||
| 012889e51d | |||
| 3b3788b722 | |||
| 9414bbc6ab | |||
| 287aabc9a3 | |||
| 2d505048c5 | |||
| e07cbb9164 |
@@ -0,0 +1,190 @@
|
||||
name: Pull request standards
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- ready_for_review
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check pull request follows contribution standards
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # To label and comment on pull requests
|
||||
steps:
|
||||
- name: Check pull request standards
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
||||
if (pr.user.type === "Bot") {
|
||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
||||
return;
|
||||
}
|
||||
if (pr.draft) {
|
||||
core.info("Skipping draft pull request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: pr.user.login,
|
||||
});
|
||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`${pr.user.login} is not an organization member, checking standards`);
|
||||
}
|
||||
|
||||
const label = "Needs Template";
|
||||
const marker = "<!-- pr-standards-check -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = pr.number;
|
||||
|
||||
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
||||
const ignoreMissing = async (fn) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info("Target already removed, nothing to do");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
||||
const setMinimized = async (subjectId, minimized) => {
|
||||
const mutation = minimized
|
||||
? `mutation($id: ID!) {
|
||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`
|
||||
: `mutation($id: ID!) {
|
||||
unminimizeComment(input: { subjectId: $id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await github.graphql(mutation, { id: subjectId });
|
||||
} catch (error) {
|
||||
core.info(
|
||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Content of a "## <name>" section, or null when the heading is absent.
|
||||
const section = (name) => {
|
||||
const match = body.match(
|
||||
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const problems = [];
|
||||
|
||||
const requiredHeadings = [
|
||||
"## proposed change",
|
||||
"## type of change",
|
||||
"## checklist",
|
||||
];
|
||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
||||
problems.push(
|
||||
"Use the pull request template without removing its sections."
|
||||
);
|
||||
}
|
||||
|
||||
const typeOfChange = section("type of change");
|
||||
if (typeOfChange !== null) {
|
||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
||||
if (ticked !== 1) {
|
||||
problems.push(
|
||||
'Select exactly one option under "Type of change".'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const proposedChange = section("proposed change");
|
||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
||||
problems.push('Describe your changes under "Proposed change".');
|
||||
}
|
||||
|
||||
const isValid = problems.length === 0;
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number, per_page: 100 }
|
||||
);
|
||||
const existing = comments.find((c) => c.body.includes(marker));
|
||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
||||
|
||||
if (isValid) {
|
||||
core.info("Pull request standards met");
|
||||
|
||||
if (hasLabel) {
|
||||
await ignoreMissing(() =>
|
||||
github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number, name: label,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existing) {
|
||||
await setMinimized(existing.node_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
`${marker}\n` +
|
||||
`Hey @${pr.user.login}!\n\n` +
|
||||
`Thank you for your contribution! To help reviewers, please update ` +
|
||||
`this pull request to follow our pull request standards:\n\n` +
|
||||
problems.map((p) => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body: message,
|
||||
});
|
||||
await setMinimized(existing.node_id, false);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number, body: message,
|
||||
});
|
||||
}
|
||||
|
||||
// Fail this check so it can block the PR from being merged
|
||||
core.setFailed(
|
||||
`Pull request standards not met:\n- ${problems.join("\n- ")}`
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
diff --git a/lib/cook-raw-quasi.js b/lib/cook-raw-quasi.js
|
||||
index 3ea8fa7be8e357c1066d7417caeeecd841415208..6bf04ab0bed8897b5ff2898ca835867aec5cee6a 100644
|
||||
--- a/lib/cook-raw-quasi.js
|
||||
+++ b/lib/cook-raw-quasi.js
|
||||
@@ -1,10 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
-function cookRawQuasi({transform}, raw) {
|
||||
+function cookRawQuasi({transformSync}, raw) {
|
||||
// This nasty hack is needed until https://github.com/babel/babel/issues/9242 is resolved.
|
||||
const args = {raw};
|
||||
|
||||
- transform('cooked`' + args.raw + '`', {
|
||||
+ // Babel 8 removed synchronous `transform`; use `transformSync` instead.
|
||||
+ transformSync('cooked`' + args.raw + '`', {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
plugins: [
|
||||
+325
-325
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.16.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.17.0.cjs
|
||||
|
||||
@@ -104,7 +104,6 @@ module.exports.babelOptions = ({
|
||||
{
|
||||
useBuiltIns: "usage",
|
||||
corejs: dependencies["core-js"],
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { DemoTrace } from "./types";
|
||||
|
||||
export const notTriggeredTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_step: "trigger/0",
|
||||
run_id: "788767ce152d3d4475134bf1107986d4",
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
timestamp: {
|
||||
start: "2021-03-25T04:36:51.223337+00:00",
|
||||
finish: "2021-03-25T04:36:51.223341+00:00",
|
||||
},
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
domain: "automation",
|
||||
item_id: "1781703842452",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
timestamp: "2021-03-25T04:36:51.223340+00:00",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: {
|
||||
entity_id: "light.bed_light",
|
||||
to_state: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
config: {
|
||||
id: "1781703842452",
|
||||
alias: "Light Turned On Notification",
|
||||
description: "Send a notification when a specific light is turned on.",
|
||||
triggers: [
|
||||
{
|
||||
trigger: "light.turned_on",
|
||||
target: {
|
||||
floor_id: "test",
|
||||
},
|
||||
options: {
|
||||
for: "00:00:00",
|
||||
behavior: "each",
|
||||
},
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
actions: [
|
||||
{
|
||||
action: "notify.notify",
|
||||
data: {
|
||||
message: "A light was turned on.",
|
||||
},
|
||||
},
|
||||
],
|
||||
mode: "single",
|
||||
},
|
||||
context: {
|
||||
id: "01KVAX7CG7XBDYGJYAGA4XJHGX",
|
||||
parent_id: "01KVAX7CG631JRX4H3JS5JJ11Q",
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
logbookEntries: [],
|
||||
};
|
||||
@@ -24,6 +24,33 @@ const traces: DemoTrace[] = [
|
||||
error: 'Variable "beer" cannot be None',
|
||||
}),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
|
||||
mockDemoTrace({
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: { entity_id: "light.bed_light", to_state: "off" },
|
||||
},
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-automation-trace-timeline")
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/trace/ha-trace-path-details";
|
||||
import type { HatScriptGraph } from "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-trace-timeline";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { basicTrace } from "../../data/traces/basic_trace";
|
||||
import { motionLightTrace } from "../../data/traces/motion-light-trace";
|
||||
import { notTriggeredTrace } from "../../data/traces/not-triggered-trace";
|
||||
import type { DemoTrace } from "../../data/traces/types";
|
||||
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace, notTriggeredTrace];
|
||||
|
||||
@customElement("demo-automation-trace")
|
||||
export class DemoAutomationTrace extends LitElement {
|
||||
@@ -20,18 +23,25 @@ export class DemoAutomationTrace extends LitElement {
|
||||
|
||||
@state() private _selected = {};
|
||||
|
||||
@queryAll("hat-script-graph") private _graphs!: NodeListOf<HatScriptGraph>;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
${traces.map(
|
||||
(trace, idx) => html`
|
||||
${traces.map((trace, idx) => {
|
||||
const graph = this._graphs?.[idx];
|
||||
const selectedPath = this._selected[idx];
|
||||
const selectedNode = selectedPath
|
||||
? graph?.renderedNodes[selectedPath]
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-card .header=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-script-graph
|
||||
.trace=${trace.trace}
|
||||
.selected=${this._selected[idx]}
|
||||
.selected=${selectedPath}
|
||||
@graph-node-selected=${this._handleGraphNodeSelected}
|
||||
.sampleIdx=${idx}
|
||||
></hat-script-graph>
|
||||
@@ -40,15 +50,25 @@ export class DemoAutomationTrace extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.selectedPath=${this._selected[idx]}
|
||||
.selectedPath=${selectedPath}
|
||||
@value-changed=${this._handleTimelineValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></hat-trace-timeline>
|
||||
${selectedNode && graph
|
||||
? html`<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedNode}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.trackedNodes=${graph.trackedNodes}
|
||||
.renderedNodes=${graph.renderedNodes}
|
||||
></ha-trace-path-details>`
|
||||
: nothing}
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
@@ -14,6 +15,11 @@ import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../src/data/context";
|
||||
import { updateHassGroups } from "../../../../src/data/context/updateContext";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
@@ -496,6 +502,10 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
label: "Password",
|
||||
selector: { text: { type: "password" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -518,6 +528,17 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
// The date/datetime selectors and the date-picker dialog consume these
|
||||
// contexts (provided by the root element in the real app). Provide them here
|
||||
// so they work in the gallery.
|
||||
private _i18nProvider = new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
});
|
||||
|
||||
private _configProvider = new ContextProvider(this, {
|
||||
context: configContext,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
@@ -539,6 +560,16 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
this._i18nProvider.setValue(
|
||||
updateHassGroups.internationalization(this.hass)
|
||||
);
|
||||
this._configProvider.setValue(updateHassGroups.config(this.hass));
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
|
||||
@@ -353,7 +353,6 @@ export class DemoEntityState extends LitElement {
|
||||
title: "Icon",
|
||||
template: (entry) => html`
|
||||
<state-badge
|
||||
.hass=${hass}
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
|
||||
+25
-24
@@ -28,7 +28,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
@@ -36,26 +36,26 @@
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.9",
|
||||
"@formatjs/intl-displaynames": "7.3.10",
|
||||
"@formatjs/intl-durationformat": "0.10.14",
|
||||
"@formatjs/intl-durationformat": "0.10.15",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.10",
|
||||
"@formatjs/intl-listformat": "8.3.10",
|
||||
"@formatjs/intl-locale": "5.3.9",
|
||||
"@formatjs/intl-numberformat": "9.3.11",
|
||||
"@formatjs/intl-pluralrules": "6.3.10",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.10",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@fullcalendar/core": "6.1.21",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"@fullcalendar/interaction": "6.1.21",
|
||||
"@fullcalendar/list": "6.1.21",
|
||||
"@fullcalendar/luxon3": "6.1.21",
|
||||
"@fullcalendar/timegrid": "6.1.21",
|
||||
"@home-assistant/webawesome": "3.7.0-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
@@ -63,6 +63,7 @@
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@lit/task": "1.0.3",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/web": "2.4.1",
|
||||
@@ -71,8 +72,8 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.1.3",
|
||||
"@tsparticles/preset-links": "4.1.3",
|
||||
"@tsparticles/engine": "4.2.1",
|
||||
"@tsparticles/preset-links": "4.2.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -126,10 +127,10 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@babel/core": "8.0.0",
|
||||
"@babel/helper-define-polyfill-provider": "1.0.0",
|
||||
"@babel/plugin-transform-runtime": "8.0.0",
|
||||
"@babel/preset-env": "8.0.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.62.0",
|
||||
@@ -137,7 +138,7 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.13",
|
||||
"@rsdoctor/rspack-plugin": "1.5.15",
|
||||
"@rspack/core": "2.0.8",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -150,13 +151,13 @@
|
||||
"@types/leaflet-draw": "1.0.13",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/luxon": "3.7.2",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"babel-plugin-template-html-minifier": "patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
@@ -195,9 +196,9 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"typescript-eslint": "8.61.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.8",
|
||||
"vitest": "4.1.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
@@ -207,14 +208,14 @@
|
||||
"lit-html": "3.3.3",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.6.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.16.0",
|
||||
"packageManager": "yarn@4.17.0",
|
||||
"volta": {
|
||||
"node": "24.16.0"
|
||||
"node": "24.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ensureArray } from "../array/ensure-array";
|
||||
import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
isCore(page) || isLoadedIntegration(hass, page);
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
(!page.filter || page.filter(hass));
|
||||
|
||||
export const isLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -110,6 +110,25 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
|
||||
"media_player",
|
||||
]);
|
||||
|
||||
/** Domains that use a timestamp for state. */
|
||||
export const TIMESTAMP_STATE_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
/** Temperature units. */
|
||||
export const UNIT_C = "°C";
|
||||
export const UNIT_F = "°F";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Task, type TaskConfig } from "@lit/task";
|
||||
import type { ReactiveControllerHost } from "lit";
|
||||
|
||||
/**
|
||||
* A `@lit/task` Task with a sticky `resolved` flag: false until the task has
|
||||
* completed once, then true. Lets callers tell "still loading" apart from
|
||||
* "resolved with an empty value" without a null sentinel, while keeping the
|
||||
* previous value during a re-run.
|
||||
*/
|
||||
export class AsyncValueTask<T extends readonly unknown[], R> extends Task<
|
||||
T,
|
||||
R
|
||||
> {
|
||||
private _resolved = false;
|
||||
|
||||
constructor(host: ReactiveControllerHost, config: TaskConfig<T, R>) {
|
||||
super(host, {
|
||||
...config,
|
||||
onComplete: (value) => {
|
||||
this._resolved = true;
|
||||
config.onComplete?.(value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public get resolved(): boolean {
|
||||
return this._resolved;
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,24 @@ import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { selectUnit } from "../util/select-unit";
|
||||
|
||||
const formatRelTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
|
||||
(locale: FrontendLocaleData, style: Intl.RelativeTimeFormatStyle) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style })
|
||||
);
|
||||
|
||||
export const relativeTime = (
|
||||
from: Date,
|
||||
locale: FrontendLocaleData,
|
||||
to?: Date,
|
||||
includeTense = true
|
||||
includeTense = true,
|
||||
style: Intl.RelativeTimeFormatStyle = "long"
|
||||
): string => {
|
||||
const diff = selectUnit(from, to, locale);
|
||||
if (includeTense) {
|
||||
return formatRelTimeMem(locale).format(diff.value, diff.unit);
|
||||
return formatRelTimeMem(locale, style).format(diff.value, diff.unit);
|
||||
}
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: diff.unit,
|
||||
unitDisplay: "long",
|
||||
unitDisplay: style,
|
||||
}).format(Math.abs(diff.value));
|
||||
};
|
||||
|
||||
@@ -60,6 +60,17 @@ export const computeAttributeValueToParts = (
|
||||
return [{ type: "value", value: localize("state.default.unknown") }];
|
||||
}
|
||||
|
||||
// Device class attribute, return the integration's translated name
|
||||
if (attribute === "device_class" && typeof attributeValue === "string") {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const deviceClassName = localize(
|
||||
`component.${domain}.entity_component.${attributeValue}.name`
|
||||
);
|
||||
if (deviceClassName) {
|
||||
return [{ type: "value", value: deviceClassName }];
|
||||
}
|
||||
}
|
||||
|
||||
// Number value, return formatted number
|
||||
if (typeof attributeValue === "number") {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { unitFromParts } from "./value_parts";
|
||||
|
||||
interface EntityUnitStubConfig {
|
||||
entity: string;
|
||||
@@ -40,5 +41,5 @@ export const computeEntityUnitDisplay = (
|
||||
? hass.formatEntityAttributeValueToParts(stateObj, config.attribute)
|
||||
: hass.formatEntityStateToParts(stateObj);
|
||||
|
||||
return parts.find((part) => part.type === "unit")?.value ?? "";
|
||||
return unitFromParts(parts);
|
||||
};
|
||||
|
||||
@@ -21,29 +21,11 @@ import {
|
||||
isNumericSensorDeviceClass,
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES,
|
||||
} from "../../data/sensor";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../const";
|
||||
|
||||
// Domains whose state is a timezone-agnostic date and/or time string.
|
||||
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
|
||||
|
||||
// Domains whose state is a timestamp.
|
||||
const TIMESTAMP_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
// Maps Intl.NumberFormat part types to ValuePart types for monetary states.
|
||||
const MONETARY_TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
@@ -178,7 +160,8 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const type = MONETARY_TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
// Merge consecutive value parts so the number stays a single part
|
||||
// (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
@@ -273,7 +256,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
TIMESTAMP_DOMAINS.has(domain) ||
|
||||
TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(attributes.device_class))
|
||||
) {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export type FeatureClassNames<T extends number = number> = Partial<
|
||||
Record<T, string>
|
||||
>;
|
||||
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export const featureClassNames = (
|
||||
stateObj: HassEntity,
|
||||
classNames: FeatureClassNames
|
||||
) => {
|
||||
if (!stateObj || !stateObj.attributes.supported_features) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) =>
|
||||
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { AITaskEntityFeature } from "../../data/ai_task";
|
||||
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
|
||||
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
|
||||
import { CalendarEntityFeature } from "../../data/calendar";
|
||||
import { CameraEntityFeature } from "../../data/camera";
|
||||
import { ClimateEntityFeature } from "../../data/climate";
|
||||
import { ConversationEntityFeature } from "../../data/conversation";
|
||||
import { CoverEntityFeature } from "../../data/cover";
|
||||
import { FanEntityFeature } from "../../data/fan";
|
||||
import { HumidifierEntityFeature } from "../../data/humidifier";
|
||||
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
|
||||
import { LightEntityFeature } from "../../data/light";
|
||||
import { LockEntityFeature } from "../../data/lock";
|
||||
import { MediaPlayerEntityFeature } from "../../data/media-player";
|
||||
import { NotifyEntityFeature } from "../../data/notify";
|
||||
import { RemoteEntityFeature } from "../../data/remote";
|
||||
import { SirenEntityFeature } from "../../data/siren";
|
||||
import { TodoListEntityFeature } from "../../data/todo";
|
||||
import { UpdateEntityFeature } from "../../data/update";
|
||||
import { VacuumEntityFeature } from "../../data/vacuum";
|
||||
import { ValveEntityFeature } from "../../data/valve";
|
||||
import { WaterHeaterEntityFeature } from "../../data/water_heater";
|
||||
import { WeatherEntityFeature } from "../../data/weather";
|
||||
|
||||
export type FeatureEnum = Record<string | number, string | number>;
|
||||
|
||||
const DOMAIN_ENUMS = {
|
||||
ai_task: AITaskEntityFeature,
|
||||
alarm_control_panel: AlarmControlPanelEntityFeature,
|
||||
assist_satellite: AssistSatelliteEntityFeature,
|
||||
calendar: CalendarEntityFeature,
|
||||
camera: CameraEntityFeature,
|
||||
climate: ClimateEntityFeature,
|
||||
conversation: ConversationEntityFeature,
|
||||
cover: CoverEntityFeature,
|
||||
fan: FanEntityFeature,
|
||||
humidifier: HumidifierEntityFeature,
|
||||
lawn_mower: LawnMowerEntityFeature,
|
||||
light: LightEntityFeature,
|
||||
lock: LockEntityFeature,
|
||||
media_player: MediaPlayerEntityFeature,
|
||||
notify: NotifyEntityFeature,
|
||||
remote: RemoteEntityFeature,
|
||||
siren: SirenEntityFeature,
|
||||
todo: TodoListEntityFeature,
|
||||
update: UpdateEntityFeature,
|
||||
vacuum: VacuumEntityFeature,
|
||||
valve: ValveEntityFeature,
|
||||
water_heater: WaterHeaterEntityFeature,
|
||||
weather: WeatherEntityFeature,
|
||||
};
|
||||
|
||||
export function getFeatures(domain: string): FeatureEnum | undefined {
|
||||
const enumObj = DOMAIN_ENUMS[domain] as FeatureEnum;
|
||||
return enumObj;
|
||||
}
|
||||
@@ -22,16 +22,13 @@ export const FIXED_DOMAIN_STATES = {
|
||||
assist_satellite: ["idle", "listening", "responding", "processing"],
|
||||
automation: ["on", "off"],
|
||||
binary_sensor: ["on", "off"],
|
||||
button: [],
|
||||
calendar: ["on", "off"],
|
||||
camera: ["idle", "recording", "streaming"],
|
||||
cover: ["closed", "closing", "open", "opening"],
|
||||
device_tracker: ["home", "not_home"],
|
||||
fan: ["on", "off"],
|
||||
humidifier: ["on", "off"],
|
||||
infrared: [],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: [
|
||||
@@ -56,7 +53,6 @@ export const FIXED_DOMAIN_STATES = {
|
||||
plant: ["ok", "problem"],
|
||||
radio_frequency: [],
|
||||
remote: ["on", "off"],
|
||||
scene: [],
|
||||
schedule: ["on", "off"],
|
||||
script: ["on", "off"],
|
||||
siren: ["on", "off"],
|
||||
@@ -290,6 +286,81 @@ export const getStatesDomain = (
|
||||
return result;
|
||||
};
|
||||
|
||||
// Maps a value attribute (or the main state, keyed `_`) to the attribute listing
|
||||
// its options. Naming is irregular per domain, so it's mapped explicitly.
|
||||
export const DOMAIN_OPTIONS_ATTRIBUTES: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = {
|
||||
climate: {
|
||||
_: "hvac_modes",
|
||||
fan_mode: "fan_modes",
|
||||
preset_mode: "preset_modes",
|
||||
swing_mode: "swing_modes",
|
||||
swing_horizontal_mode: "swing_horizontal_modes",
|
||||
},
|
||||
event: {
|
||||
event_type: "event_types",
|
||||
},
|
||||
fan: {
|
||||
preset_mode: "preset_modes",
|
||||
},
|
||||
humidifier: {
|
||||
mode: "available_modes",
|
||||
},
|
||||
input_select: {
|
||||
_: "options",
|
||||
},
|
||||
select: {
|
||||
_: "options",
|
||||
},
|
||||
light: {
|
||||
effect: "effect_list",
|
||||
color_mode: "supported_color_modes",
|
||||
},
|
||||
media_player: {
|
||||
sound_mode: "sound_mode_list",
|
||||
source: "source_list",
|
||||
},
|
||||
remote: {
|
||||
current_activity: "activity_list",
|
||||
},
|
||||
sensor: {
|
||||
_: "options",
|
||||
},
|
||||
vacuum: {
|
||||
fan_speed: "fan_speed_list",
|
||||
},
|
||||
water_heater: {
|
||||
_: "operation_list",
|
||||
operation_mode: "operation_list",
|
||||
},
|
||||
};
|
||||
|
||||
const DOMAIN_VALUE_ATTRIBUTES: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = Object.fromEntries(
|
||||
Object.entries(DOMAIN_OPTIONS_ATTRIBUTES).map(([domain, mapping]) => [
|
||||
domain,
|
||||
Object.fromEntries(
|
||||
Object.entries(mapping).map(([value, list]) => [list, value])
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
// value attribute (or main state) → its options-list attribute
|
||||
export const getOptionsAttribute = (
|
||||
domain: string,
|
||||
attribute?: string
|
||||
): string | undefined => DOMAIN_OPTIONS_ATTRIBUTES[domain]?.[attribute ?? "_"];
|
||||
|
||||
// options-list attribute → its value attribute (`_` = main state)
|
||||
export const getValueAttribute = (
|
||||
domain: string,
|
||||
optionsAttribute: string
|
||||
): string | undefined => DOMAIN_VALUE_ATTRIBUTES[domain]?.[optionsAttribute];
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
@@ -302,78 +373,15 @@ export const getStates = (
|
||||
result.push(...getStatesDomain(hass, domain, attribute));
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
if (attribute === "event_type") {
|
||||
result.push(...state.attributes.event_types);
|
||||
}
|
||||
break;
|
||||
case "fan":
|
||||
if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
}
|
||||
break;
|
||||
case "humidifier":
|
||||
if (attribute === "mode") {
|
||||
result.push(...state.attributes.available_modes);
|
||||
}
|
||||
break;
|
||||
case "input_select":
|
||||
case "select":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.options);
|
||||
}
|
||||
break;
|
||||
case "light":
|
||||
if (attribute === "effect" && state.attributes.effect_list) {
|
||||
result.push(...state.attributes.effect_list);
|
||||
} else if (
|
||||
attribute === "color_mode" &&
|
||||
state.attributes.supported_color_modes
|
||||
) {
|
||||
result.push(...state.attributes.supported_color_modes);
|
||||
}
|
||||
break;
|
||||
case "media_player":
|
||||
if (attribute === "sound_mode") {
|
||||
result.push(...state.attributes.sound_mode_list);
|
||||
} else if (attribute === "source") {
|
||||
result.push(...state.attributes.source_list);
|
||||
}
|
||||
break;
|
||||
case "remote":
|
||||
if (attribute === "current_activity") {
|
||||
result.push(...state.attributes.activity_list);
|
||||
}
|
||||
break;
|
||||
case "sensor":
|
||||
if (!attribute && state.attributes.device_class === "enum") {
|
||||
result.push(...state.attributes.options);
|
||||
}
|
||||
break;
|
||||
case "vacuum":
|
||||
if (attribute === "fan_speed") {
|
||||
result.push(...state.attributes.fan_speed_list);
|
||||
}
|
||||
break;
|
||||
case "water_heater":
|
||||
if (!attribute || attribute === "operation_mode") {
|
||||
result.push(...state.attributes.operation_list);
|
||||
}
|
||||
break;
|
||||
const optionsAttribute = getOptionsAttribute(domain, attribute);
|
||||
if (optionsAttribute) {
|
||||
const options = state.attributes[optionsAttribute];
|
||||
// Sensors only expose their options when their device class is `enum`.
|
||||
const enumSensor =
|
||||
domain !== "sensor" || state.attributes.device_class === "enum";
|
||||
if (enumSensor && Array.isArray(options)) {
|
||||
result.push(...options);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(result)];
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../const";
|
||||
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (
|
||||
[
|
||||
"button",
|
||||
"event",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
].includes(domain)
|
||||
) {
|
||||
if (TIMESTAMP_STATE_DOMAINS.has(domain)) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ValuePart } from "../../types";
|
||||
|
||||
// Joins every part except the unit, keeping native order so the sign and
|
||||
// grouping stay with the value (e.g. "-2,548.14").
|
||||
export const valueFromParts = (parts: ValuePart[]): string =>
|
||||
parts
|
||||
.filter((part) => part.type !== "unit")
|
||||
.map((part) => part.value)
|
||||
.join("")
|
||||
.trim();
|
||||
|
||||
export const unitFromParts = (parts: ValuePart[]): string =>
|
||||
parts.find((part) => part.type === "unit")?.value ?? "";
|
||||
|
||||
export type UnitPosition = "before" | "after";
|
||||
|
||||
// Whether the unit sits before or after the value in the locale's native order
|
||||
// (e.g. "$5" / "€ 5" → "before", "5 €" / "5 %" → "after").
|
||||
export const unitPosition = (parts: ValuePart[]): UnitPosition => {
|
||||
const unitIndex = parts.findIndex((part) => part.type === "unit");
|
||||
if (unitIndex === -1) {
|
||||
return "after";
|
||||
}
|
||||
const lastValueIndex = parts.reduceRight(
|
||||
(acc, part, i) => (acc === -1 && part.type === "value" ? i : acc),
|
||||
-1
|
||||
);
|
||||
return unitIndex < lastValueIndex ? "before" : "after";
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
@@ -8,14 +8,30 @@ export interface RelatedIdSets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* Build a set of related IDs, merging in the current (queried) item.
|
||||
* `search/related` does not echo the queried item back, but it is the closest
|
||||
* related item (e.g. a card editor's own entity), so it is merged into the
|
||||
* matching group when it is an area, device, or entity.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
* @param current - The queried item to merge in.
|
||||
* @returns The related ID sets, including the current item.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
export const buildRelatedIdSets = (
|
||||
related?: RelatedResult,
|
||||
current?: { itemType: ItemType; itemId: string }
|
||||
): RelatedIdSets => ({
|
||||
areas: new Set([
|
||||
...(related?.area || []),
|
||||
...(current?.itemType === "area" ? [current.itemId] : []),
|
||||
]),
|
||||
devices: new Set([
|
||||
...(related?.device || []),
|
||||
...(current?.itemType === "device" ? [current.itemId] : []),
|
||||
]),
|
||||
entities: new Set([
|
||||
...(related?.entity || []),
|
||||
...(current?.itemType === "entity" ? [current.itemId] : []),
|
||||
]),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,8 +17,6 @@ export type LocalizeKeys =
|
||||
| `ui.common.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
| `ui.components.logbook.messages.cleared_device_classes.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Return a shallow copy of an object with every key removed whose value is
|
||||
* `undefined` or equals that key's default, so a key left at its default
|
||||
* (whether absent or explicit) does not count as a difference. A key's default
|
||||
* comes from `defaults` when present, otherwise `false`.
|
||||
*
|
||||
* Non-plain-object values are returned unchanged; only top-level keys are
|
||||
* compared.
|
||||
*/
|
||||
export const stripDefaults = <T>(
|
||||
value: T,
|
||||
defaults?: Record<string, unknown>
|
||||
): T => {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
const defaultValue = defaults && key in defaults ? defaults[key] : false;
|
||||
if (val === undefined || val === defaultValue) {
|
||||
continue;
|
||||
}
|
||||
result[key] = val;
|
||||
}
|
||||
return result as T;
|
||||
};
|
||||
@@ -4,10 +4,10 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
|
||||
|
||||
/**
|
||||
* Call a function with result caching per entity.
|
||||
* @param cacheKey key to store the cache on hass object
|
||||
* @param cacheKey key to namespace the cache
|
||||
* @param cacheTime time to cache the results
|
||||
* @param func function to fetch the data
|
||||
* @param hass Home Assistant object
|
||||
* @param hass Home Assistant object (or slice) the cache is keyed on
|
||||
* @param entityId entity to fetch data for
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
@@ -15,8 +15,12 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
|
||||
export const timeCacheEntityPromiseFunc = async <T>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
|
||||
hass: HomeAssistant,
|
||||
func: (
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
) => Promise<T>,
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
@@ -39,11 +43,11 @@ export const timeCacheEntityPromiseFunc = async <T>(
|
||||
// When successful, set timer to clear cache
|
||||
() =>
|
||||
setTimeout(() => {
|
||||
cache![entityId] = undefined;
|
||||
cache[entityId] = undefined;
|
||||
}, cacheTime),
|
||||
// On failure, clear cache right away
|
||||
() => {
|
||||
cache![entityId] = undefined;
|
||||
cache[entityId] = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ interface CacheResult<T> {
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
*/
|
||||
export const timeCachePromiseFunc = async <T>(
|
||||
export const timeCachePromiseFunc = async <T, H = HomeAssistant>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
|
||||
generateCacheKey:
|
||||
| ((hass: HomeAssistant, lastResult: T) => unknown)
|
||||
| undefined,
|
||||
hass: HomeAssistant,
|
||||
func: (hass: H, ...args: any[]) => Promise<T>,
|
||||
generateCacheKey: ((hass: H, lastResult: T) => unknown) | undefined,
|
||||
hass: H,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
const anyHass = hass as any;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiCloseCircle,
|
||||
mdiHelpCircle,
|
||||
} from "@mdi/js";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -19,46 +26,59 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
private get _iconPath() {
|
||||
switch (this.state) {
|
||||
case "pass":
|
||||
return mdiCheckCircle;
|
||||
case "fail":
|
||||
return mdiCloseCircle;
|
||||
case "invalid":
|
||||
return mdiAlertCircle;
|
||||
default:
|
||||
return mdiHelpCircle;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
id="indicator"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
|
||||
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
top: -8px;
|
||||
inset-inline-end: -8px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: var(--ha-border-width-md) solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
#indicator ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
color: var(--ha-color-green-60);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-orange-60);
|
||||
color: var(--ha-color-orange-60);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-red-60);
|
||||
color: var(--ha-color-red-60);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-progress-button";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { apiContext } from "../../data/context";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { Appearance } from "../ha-button";
|
||||
|
||||
@customElement("ha-call-service-button")
|
||||
class HaCallServiceButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -56,7 +58,7 @@ class HaCallServiceButton extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||
|
||||
try {
|
||||
await this.hass.callService(
|
||||
await this._api.callService(
|
||||
this.domain,
|
||||
this.service,
|
||||
this.data,
|
||||
|
||||
@@ -445,6 +445,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
|
||||
@@ -552,6 +552,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) =>
|
||||
formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
|
||||
|
||||
@@ -79,9 +79,11 @@ function computeTimelineEnumColor(
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const states =
|
||||
FIXED_DOMAIN_STATES[domain] ||
|
||||
(domain === "sensor" &&
|
||||
stateObj.attributes.device_class === "enum" &&
|
||||
stateObj.attributes.options) ||
|
||||
((domain === "sensor" && stateObj.attributes.device_class === "enum") ||
|
||||
domain === "select" ||
|
||||
domain === "input_select"
|
||||
? stateObj.attributes.options
|
||||
: undefined) ||
|
||||
[];
|
||||
const idx = states.indexOf(state);
|
||||
if (idx === -1) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -6,10 +7,14 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
markEntitiesRelated,
|
||||
sortEntitiesByRelatedRank,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -131,6 +136,20 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
private get _hasRelatedContext(): boolean {
|
||||
const related = this._relatedIdSets;
|
||||
return (
|
||||
!!related &&
|
||||
(related.entities.size > 0 ||
|
||||
related.devices.size > 0 ||
|
||||
related.areas.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
@@ -161,11 +180,7 @@ export class HaEntityPicker extends LitElement {
|
||||
: undefined;
|
||||
if (stateObj) {
|
||||
return html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
<state-badge slot="start" .stateObj=${stateObj}></state-badge>
|
||||
`;
|
||||
}
|
||||
if (extraOption.icon_path) {
|
||||
@@ -216,11 +231,7 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<state-badge .stateObj=${stateObj} slot="start"></state-badge>
|
||||
<span slot="headline">${primary}</span>
|
||||
<span slot="supporting-text">${secondary}</span>
|
||||
`;
|
||||
@@ -250,7 +261,6 @@ export class HaEntityPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
@@ -333,8 +343,22 @@ export class HaEntityPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _sortByRelatedContext = memoizeOne(
|
||||
(
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
language: string
|
||||
): EntityComboBoxItem[] =>
|
||||
sortEntitiesByRelatedRank(
|
||||
markEntitiesRelated(items, related, entities, devices),
|
||||
language
|
||||
)
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
const entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -345,14 +369,23 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value
|
||||
);
|
||||
const sortedItems = this._hasRelatedContext
|
||||
? this._sortByRelatedContext(
|
||||
entityItems,
|
||||
this._relatedIdSets!,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: entityItems;
|
||||
if (this.extraOptions?.length) {
|
||||
const resolvedExtras = this.extraOptions.map((opt) => ({
|
||||
...opt,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...items];
|
||||
return [...resolvedExtras, ...sortedItems];
|
||||
}
|
||||
return items;
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
@@ -384,6 +417,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.noSort=${this._hasRelatedContext}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
@@ -402,17 +436,23 @@ export class HaEntityPicker extends LitElement {
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// Float related items to the top by closeness, keeping search relevance
|
||||
// order within each tier.
|
||||
const items = this._hasRelatedContext
|
||||
? sortEntitiesByRelatedRank(filteredItems)
|
||||
: filteredItems;
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex(
|
||||
const index = items.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
return items;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
const [exactMatch] = items.splice(index, 1);
|
||||
items.unshift(exactMatch);
|
||||
return items;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFlash, mdiFlashOff } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -6,9 +7,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { apiContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-switch";
|
||||
@@ -29,8 +30,8 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@@ -118,7 +119,7 @@ export class HaEntityToggle extends LitElement {
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
private async _callService(turnOn): Promise<void> {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._api || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic(this, "light");
|
||||
@@ -149,7 +150,7 @@ export class HaEntityToggle extends LitElement {
|
||||
this._isOn = turnOn;
|
||||
|
||||
try {
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
await this._api.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -6,13 +8,19 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import {
|
||||
consumeEntityRegistryEntry,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { unitFromParts, valueFromParts } from "../../common/entity/value_parts";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { formattersContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@@ -40,7 +48,15 @@ const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consumeEntityRegistryEntry({ entityIdPath: ["state", "entity_id"] })
|
||||
private _entry?: EntityRegistryDisplayEntry;
|
||||
|
||||
@property({ attribute: false }) public state?: HassEntity;
|
||||
|
||||
@@ -77,10 +93,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return html`
|
||||
<ha-label-badge
|
||||
class="warning"
|
||||
label=${this.hass!.localize("state_badge.default.error")}
|
||||
description=${this.hass!.localize(
|
||||
"state_badge.default.entity_not_found"
|
||||
)}
|
||||
label=${this._localize("state_badge.default.error")}
|
||||
description=${this._localize("state_badge.default.entity_not_found")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
@@ -94,7 +108,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
// 4. Icon determined via entity state
|
||||
// 5. Value string as fallback
|
||||
const domain = computeStateDomain(entityState);
|
||||
const entry = this.hass?.entities[entityState.entity_id];
|
||||
const entry = this._entry;
|
||||
|
||||
const showIcon =
|
||||
this.icon || this._computeShowIcon(domain, entityState, entry);
|
||||
@@ -163,20 +177,23 @@ export class HaStateLabelBadge extends LitElement {
|
||||
case "sun":
|
||||
case "timer":
|
||||
return null;
|
||||
// @ts-expect-error we don't break and go to default
|
||||
case "sensor":
|
||||
if (entry?.platform === "moon") {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
break;
|
||||
default:
|
||||
return entityState.state === UNAVAILABLE ||
|
||||
entityState.state === UNKNOWN
|
||||
? "—"
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
)?.value;
|
||||
break;
|
||||
}
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return "—";
|
||||
}
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return valueFromParts(
|
||||
this._formatters.formatEntityStateToParts(entityState)
|
||||
);
|
||||
}
|
||||
|
||||
private _computeShowIcon(
|
||||
@@ -211,11 +228,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
) {
|
||||
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||
return this._localize(`state_badge.default.${entityState.state}`);
|
||||
}
|
||||
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||
if (domainStateKey) {
|
||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||
return this._localize(`state_badge.${domainStateKey}`);
|
||||
}
|
||||
// Person and device tracker state can be zone name
|
||||
if (domain === "person" || domain === "device_tracker") {
|
||||
@@ -224,10 +241,12 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "unit"
|
||||
)?.value || null
|
||||
unitFromParts(this._formatters.formatEntityStateToParts(entityState)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -343,11 +343,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
return html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<state-badge .stateObj=${item.stateObj} slot="start"></state-badge>
|
||||
`
|
||||
: item.icon_path
|
||||
? html`
|
||||
@@ -488,7 +484,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -14,13 +15,12 @@ import {
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { connectionContext } from "../../data/context";
|
||||
import { isBrandUrl } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
export class StateBadge extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public overrideIcon?: string;
|
||||
@@ -36,6 +36,10 @@ export class StateBadge extends LitElement {
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public icon = true;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state() private _iconStyle: Record<string, string | undefined> = {};
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -106,14 +110,15 @@ export class StateBadge extends LitElement {
|
||||
></ha-state-icon>`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
!changedProps.has("stateObj") &&
|
||||
!changedProps.has("overrideImage") &&
|
||||
!changedProps.has("overrideIcon") &&
|
||||
!changedProps.has("stateColor") &&
|
||||
!changedProps.has("color")
|
||||
!changedProps.has("color") &&
|
||||
!changedProps.has("_connection")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -133,12 +138,10 @@ export class StateBadge extends LitElement {
|
||||
stateObj.attributes.entity_picture) &&
|
||||
!this.overrideIcon
|
||||
) {
|
||||
let imageUrl =
|
||||
let imageUrl = this._resolveImageUrl(
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
stateObj.attributes.entity_picture
|
||||
);
|
||||
if (domain === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
@@ -179,11 +182,7 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
} else if (this.overrideImage) {
|
||||
let imageUrl = this.overrideImage;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
|
||||
this.icon = false;
|
||||
}
|
||||
}
|
||||
@@ -192,6 +191,20 @@ export class StateBadge extends LitElement {
|
||||
this.style.backgroundImage = backgroundImage;
|
||||
}
|
||||
|
||||
// Sign the image URL via the connection context so brand images
|
||||
// (/api/brands/...) get their access token. Without a way to sign, a brands
|
||||
// request would be rejected (and logged/blocked by core), so skip it until
|
||||
// we can sign.
|
||||
private _resolveImageUrl(url: string | undefined): string {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
if (this._connection) {
|
||||
return this._connection.hassUrl(url);
|
||||
}
|
||||
return isBrandUrl(url) ? "" : url;
|
||||
}
|
||||
|
||||
protected getClass() {
|
||||
const cls = new Map(
|
||||
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
|
||||
|
||||
@@ -24,7 +24,6 @@ class StateInfo extends LitElement {
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
|
||||
@@ -173,7 +173,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -226,7 +225,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { areasContext, floorsContext } from "../data/context";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
@@ -17,7 +18,13 @@ export interface AreasDisplayValue {
|
||||
|
||||
@customElement("ha-areas-display-editor")
|
||||
export class HaAreasDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -35,10 +42,10 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areas = Object.values(this.hass.areas);
|
||||
const areas = Object.values(this._areas);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const { floor } = getAreaContext(area, this._floors);
|
||||
return {
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { areasContext, floorsContext } from "../data/context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -30,7 +34,17 @@ const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
@customElement("ha-areas-floors-display-editor")
|
||||
export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -51,13 +65,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const groupedAreasItems = this._groupedAreasItems(
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
this._areas,
|
||||
this._floors
|
||||
);
|
||||
|
||||
const filteredFloors = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).filter(
|
||||
(floor) =>
|
||||
// Only include floors that have areas assigned to them
|
||||
@@ -124,15 +139,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _groupedAreasItems = memoizeOne(
|
||||
(
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
// update items if floors change
|
||||
_hassFloors: HomeAssistant["floors"]
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>
|
||||
): Record<string, DisplayItem[]> => {
|
||||
const areas = Object.values(hassAreas);
|
||||
const areaList = Object.values(areas);
|
||||
|
||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||
const groupedItems: Record<string, DisplayItem[]> = areaList.reduce(
|
||||
(acc, area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const { floor } = getAreaContext(area, floors);
|
||||
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
|
||||
if (!acc[floorId]) {
|
||||
@@ -155,23 +169,24 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _sortedFloors = memoizeOne(
|
||||
(
|
||||
hassFloors: HomeAssistant["floors"],
|
||||
order: string[] | undefined
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
order: string[] | undefined,
|
||||
localize: LocalizeFunc
|
||||
): FloorRegistryEntry[] => {
|
||||
const floors = getFloors(hassFloors, order);
|
||||
const noFloors = floors.length === 0;
|
||||
floors.push({
|
||||
const sortedFloors = getFloors(floors, order);
|
||||
const noFloors = sortedFloors.length === 0;
|
||||
sortedFloors.push({
|
||||
floor_id: UNASSIGNED_FLOOR,
|
||||
name: noFloors
|
||||
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
? localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
return floors;
|
||||
return sortedFloors;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -180,8 +195,9 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).map((floor) => floor.floor_id);
|
||||
const newOrder = [...floorIds];
|
||||
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
|
||||
@@ -204,8 +220,9 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).map((floor) => floor.floor_id);
|
||||
|
||||
const oldAreaDisplay = this.value?.areas_display ?? {};
|
||||
@@ -223,14 +240,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
const order = oldOrder.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (order?.length) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiChevronDown,
|
||||
@@ -10,7 +11,9 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
type AssistPipeline,
|
||||
@@ -18,10 +21,19 @@ import {
|
||||
type ConversationChatLogToolResultDelta,
|
||||
type PipelineRunEvent,
|
||||
} from "../data/assist_pipeline";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import { ConversationEntityFeature } from "../data/conversation";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
} from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
@@ -47,8 +59,6 @@ interface AssistMessage {
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
@@ -71,6 +81,22 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: HomeAssistantConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: HomeAssistantConnection;
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
@@ -86,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
},
|
||||
@@ -124,9 +150,9 @@ export class HaAssistChat extends LitElement {
|
||||
const controlHA = !this.pipeline
|
||||
? false
|
||||
: this.pipeline.prefer_local_intents ||
|
||||
(this.hass.states[this.pipeline.conversation_engine]
|
||||
(this._states[this.pipeline.conversation_engine]
|
||||
? supportsFeature(
|
||||
this.hass.states[this.pipeline.conversation_engine],
|
||||
this._states[this.pipeline.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
)
|
||||
: true);
|
||||
@@ -139,7 +165,7 @@ export class HaAssistChat extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.conversation_no_control"
|
||||
)}
|
||||
</ha-alert>
|
||||
@@ -180,7 +206,7 @@ export class HaAssistChat extends LitElement {
|
||||
.path=${mdiCommentProcessingOutline}
|
||||
></ha-svg-icon>
|
||||
<span class="thinking-label">
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.show_details"
|
||||
)}
|
||||
</span>
|
||||
@@ -251,7 +277,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
id="message-input"
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._handleInput}
|
||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||
.label=${this._localize(`ui.dialogs.voice_command.input_label`)}
|
||||
>
|
||||
<div slot="end">
|
||||
${this._showSendButton || !supportsSTT
|
||||
@@ -261,7 +287,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiSend}
|
||||
@click=${this._handleSendMessage}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.send_text"
|
||||
)}
|
||||
>
|
||||
@@ -282,7 +308,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._handleListeningButton}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
@@ -374,10 +400,12 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
|
||||
private _handleToggleThinking(ev: Event) {
|
||||
const index = (ev.currentTarget as any).index;
|
||||
this._conversation[index] = {
|
||||
...this._conversation[index],
|
||||
thinking_expanded: !this._conversation[index].thinking_expanded,
|
||||
};
|
||||
// Mutate the message in place rather than replacing it. The streaming
|
||||
// processor keeps a reference to this same object and mutates it as deltas
|
||||
// arrive; swapping in a new object would detach the in-flight message from
|
||||
// the processor and freeze the chat (see #52501).
|
||||
const message = this._conversation[index];
|
||||
message.thinking_expanded = !message.thinking_expanded;
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
|
||||
@@ -391,21 +419,21 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this.hass.localize(
|
||||
html`${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
this._config,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
>${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
@@ -443,7 +471,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event: PipelineRunEvent) => {
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
@@ -539,7 +567,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
}
|
||||
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
this._connection.connection.socket!.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this._stt_binary_handler_id == undefined) {
|
||||
@@ -550,7 +578,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
data[0] = this._stt_binary_handler_id;
|
||||
data.set(new Uint8Array(chunk.buffer), 1);
|
||||
|
||||
this.hass.connection.socket!.send(data);
|
||||
this._connection.connection.socket!.send(data);
|
||||
}
|
||||
|
||||
private _unloadAudio = () => {
|
||||
@@ -570,7 +598,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
hassMessageProcesser.addMessage();
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event) => {
|
||||
if (event.type.startsWith("intent-")) {
|
||||
hassMessageProcesser.processEvent(event);
|
||||
@@ -593,7 +621,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
);
|
||||
} catch {
|
||||
hassMessageProcesser.setError(
|
||||
this.hass.localize("ui.dialogs.voice_command.error")
|
||||
this._localize("ui.dialogs.voice_command.error")
|
||||
);
|
||||
} finally {
|
||||
this._processing = false;
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import { attributeIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-attribute-icon")
|
||||
export class HaAttributeIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property() public attribute?: string;
|
||||
@@ -19,6 +24,59 @@ export class HaAttributeIcon extends LitElement {
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([
|
||||
icon,
|
||||
config,
|
||||
connection,
|
||||
entities,
|
||||
stateObj,
|
||||
attribute,
|
||||
attributeValue,
|
||||
]) => {
|
||||
if (
|
||||
icon ||
|
||||
!config ||
|
||||
!connection ||
|
||||
!entities ||
|
||||
!stateObj ||
|
||||
!attribute
|
||||
) {
|
||||
return initialState;
|
||||
}
|
||||
return attributeIcon(
|
||||
config.config,
|
||||
connection.connection,
|
||||
entities,
|
||||
stateObj,
|
||||
attribute,
|
||||
attributeValue
|
||||
);
|
||||
},
|
||||
args: () =>
|
||||
[
|
||||
this.icon,
|
||||
this._config,
|
||||
this._connection,
|
||||
this._entities,
|
||||
this.stateObj,
|
||||
this.attribute,
|
||||
this.attributeValue,
|
||||
] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -28,23 +86,13 @@ export class HaAttributeIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const icon = attributeIcon(
|
||||
this.hass,
|
||||
this.stateObj,
|
||||
this.attribute,
|
||||
this.attributeValue
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return nothing;
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: nothing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { getValueAttribute } from "../common/entity/get_states";
|
||||
import { valueFromParts } from "../common/entity/value_parts";
|
||||
import { formattersContext } from "../data/context";
|
||||
|
||||
const isObjectValue = (value: unknown): boolean =>
|
||||
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(value) && value instanceof Object);
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
class HaAttributeValue extends LitElement {
|
||||
@state()
|
||||
@@ -18,6 +26,17 @@ class HaAttributeValue extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-unit" }) public hideUnit = false;
|
||||
|
||||
private _yamlTask = new AsyncValueTask(this, {
|
||||
task: async ([attributeValue]) => {
|
||||
if (!isObjectValue(attributeValue)) {
|
||||
return initialState;
|
||||
}
|
||||
const { dump } = await import("js-yaml");
|
||||
return dump(attributeValue);
|
||||
},
|
||||
args: () => [this.stateObj?.attributes[this.attribute]] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -47,13 +66,28 @@ class HaAttributeValue extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(attributeValue) &&
|
||||
attributeValue.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||
) {
|
||||
const yaml = import("js-yaml").then(({ dump }) => dump(attributeValue));
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
if (isObjectValue(attributeValue)) {
|
||||
return html`<pre>${this._yamlTask.value ?? ""}</pre>`;
|
||||
}
|
||||
|
||||
// Options-list attributes (effect_list, preset_modes, …) translated through
|
||||
// their value attribute, or the main state for lists like hvac_modes.
|
||||
if (Array.isArray(attributeValue)) {
|
||||
const domain = computeStateDomain(this.stateObj);
|
||||
const valueAttribute = getValueAttribute(domain, this.attribute);
|
||||
if (valueAttribute) {
|
||||
return attributeValue
|
||||
.map((item) =>
|
||||
valueAttribute === "_"
|
||||
? this._formatters!.formatEntityState(this.stateObj!, item)
|
||||
: this._formatters!.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
valueAttribute,
|
||||
item
|
||||
)
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hideUnit) {
|
||||
@@ -61,10 +95,7 @@ class HaAttributeValue extends LitElement {
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
return valueFromParts(parts);
|
||||
}
|
||||
|
||||
return this._formatters!.formatEntityAttributeValue(
|
||||
|
||||
@@ -153,10 +153,16 @@ export class HaBaseTimeInput extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
|
||||
? html`<label id="label"
|
||||
>${this.label}${this.required ? " *" : ""}</label
|
||||
>`
|
||||
: nothing}
|
||||
<div class="time-input-wrap-wrap">
|
||||
<div class="time-input-wrap">
|
||||
<div
|
||||
class="time-input-wrap"
|
||||
role="group"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${this.enableDay
|
||||
? html`
|
||||
<ha-input
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@@ -15,17 +17,16 @@ export class HaBigNumber extends LitElement {
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render() {
|
||||
const formatted = formatNumber(
|
||||
this.value,
|
||||
this.hass?.locale,
|
||||
this.formatOptions
|
||||
);
|
||||
const locale = this._i18n!.locale;
|
||||
const formatted = formatNumber(this.value, locale, this.formatOptions);
|
||||
const [integer] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
@@ -33,9 +34,7 @@ export class HaBigNumber extends LitElement {
|
||||
const temperatureDecimal = formatted.replace(integer, "");
|
||||
|
||||
const formattedValue = `${this.value}${
|
||||
this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
|
||||
: ""
|
||||
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
|
||||
}`;
|
||||
|
||||
const unitBottom = this.unitPosition === "bottom";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
@@ -7,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
CameraEntityFeature,
|
||||
type CameraCapabilities,
|
||||
type CameraEntity,
|
||||
computeMJPEGStreamUrl,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
STREAM_TYPE_WEB_RTC,
|
||||
type StreamType,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, configContext, connectionContext } from "../data/context";
|
||||
import "./ha-hls-player";
|
||||
import "./ha-web-rtc-player";
|
||||
|
||||
@@ -30,7 +31,17 @@ interface Stream {
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
export class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@@ -58,21 +69,33 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||
private _thumbnailApi = memoizeOne(
|
||||
(
|
||||
api: ContextType<typeof apiContext>,
|
||||
connection: ContextType<typeof connectionContext>
|
||||
) => ({
|
||||
callWS: api.callWS,
|
||||
hassUrl: connection.hassUrl,
|
||||
})
|
||||
);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
const entityChanged =
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id;
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| ContextType<typeof configContext>
|
||||
| undefined;
|
||||
const backendStarted =
|
||||
changedProps.has("hass") &&
|
||||
this.hass &&
|
||||
changedProps.has("_config") &&
|
||||
this._config &&
|
||||
this.stateObj &&
|
||||
oldHass &&
|
||||
this.hass.config.state === STATE_RUNNING &&
|
||||
oldHass.config?.state !== STATE_RUNNING;
|
||||
oldConfig &&
|
||||
this._config.config.state === STATE_RUNNING &&
|
||||
oldConfig.config?.state !== STATE_RUNNING;
|
||||
|
||||
if (entityChanged || backendStarted) {
|
||||
this._getCapabilities();
|
||||
@@ -137,7 +160,6 @@ export class HaCameraStream extends LitElement {
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleHlsStreams}
|
||||
@@ -153,7 +175,6 @@ export class HaCameraStream extends LitElement {
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleWebRtcStreams}
|
||||
@@ -170,12 +191,12 @@ export class HaCameraStream extends LitElement {
|
||||
this._capabilities = undefined;
|
||||
this._hlsStreams = undefined;
|
||||
this._webRtcStreams = undefined;
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
if (!supportsFeature(this.stateObj!, CameraEntityFeature.STREAM)) {
|
||||
this._capabilities = { frontend_stream_types: [] };
|
||||
return;
|
||||
}
|
||||
this._capabilities = await fetchCameraCapabilities(
|
||||
this.hass!,
|
||||
this._api,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
}
|
||||
@@ -183,7 +204,7 @@ export class HaCameraStream extends LitElement {
|
||||
private async _getPosterUrl(): Promise<void> {
|
||||
try {
|
||||
this._posterUrl = await fetchThumbnailUrlWithCache(
|
||||
this.hass!,
|
||||
this._thumbnailApi(this._api, this._connection),
|
||||
this.stateObj!.entity_id,
|
||||
this.clientWidth,
|
||||
this.clientHeight
|
||||
|
||||
@@ -12,10 +12,11 @@ import {
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { HassConfig, Connection } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
@@ -57,6 +58,17 @@ export class HaConditionIcon extends LitElement {
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([icon, connection, config, condition]) => {
|
||||
if (icon || !connection || !config || !condition) {
|
||||
return initialState;
|
||||
}
|
||||
return conditionIcon(connection, config, condition);
|
||||
},
|
||||
args: () =>
|
||||
[this.icon, this._connection, this._config, this.condition] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -70,18 +82,12 @@ export class HaConditionIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = conditionIcon(
|
||||
this._connection,
|
||||
this._config,
|
||||
this.condition
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -388,7 +388,10 @@ export class HaControlSlider extends LitElement {
|
||||
private _isVisuallyInverted() {
|
||||
let inverted = this.inverted;
|
||||
|
||||
if (mainWindow.document.dir === "rtl") {
|
||||
// RTL only mirrors the horizontal axis. A vertical slider always fills
|
||||
// bottom-to-top regardless of text direction, so it must not be flipped,
|
||||
// otherwise its value mapping ends up upside down in RTL languages.
|
||||
if (!this.vertical && mainWindow.document.dir === "rtl") {
|
||||
inverted = !inverted;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { configContext, connectionContext, uiContext } from "../data/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
@@ -36,6 +37,30 @@ export class HaDomainIcon extends LitElement {
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _hassUi?: ContextType<typeof uiContext>;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([icon, connection, config, domain, deviceClass, domainState]) => {
|
||||
if (icon || !connection || !config || !domain) {
|
||||
return initialState;
|
||||
}
|
||||
return domainIcon(
|
||||
connection.connection,
|
||||
config.config,
|
||||
domain,
|
||||
deviceClass,
|
||||
domainState
|
||||
);
|
||||
},
|
||||
args: () =>
|
||||
[
|
||||
this.icon,
|
||||
this._connection,
|
||||
this._hassConfig,
|
||||
this.domain,
|
||||
this.deviceClass,
|
||||
this.state,
|
||||
] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -49,21 +74,12 @@ export class HaDomainIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = domainIcon(
|
||||
this._connection.connection,
|
||||
this._hassConfig.config,
|
||||
this.domain,
|
||||
this.deviceClass,
|
||||
this.state
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeEntityStates } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import { entityIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
|
||||
@@ -15,7 +23,21 @@ export interface EntitiesDisplayValue {
|
||||
|
||||
@customElement("ha-entities-display-editor")
|
||||
export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeEntityStates({ entityIdPath: ["entitiesIds"] })
|
||||
private _entityStates?: Record<string, HassEntity>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
@state()
|
||||
private _entitiesReg!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@state()
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@state()
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -32,20 +54,13 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const entities = this.entitiesIds
|
||||
.map((entityId) => this.hass.states[entityId])
|
||||
.filter(Boolean);
|
||||
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
const items = this._items(
|
||||
this.entitiesIds,
|
||||
this._entityStates,
|
||||
this._entitiesReg,
|
||||
this._config,
|
||||
this._connection
|
||||
);
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
@@ -61,6 +76,31 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
entitiesIds: string[],
|
||||
entityStates: Record<string, HassEntity> | undefined,
|
||||
entitiesReg: ContextType<typeof entitiesContext>,
|
||||
config: ContextType<typeof configContext>,
|
||||
connection: ContextType<typeof connectionContext>
|
||||
): DisplayItem[] => {
|
||||
const entities = entitiesIds
|
||||
.map((entityId) => entityStates?.[entityId])
|
||||
.filter((stateObj): stateObj is HassEntity => Boolean(stateObj));
|
||||
|
||||
return entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(
|
||||
entitiesReg,
|
||||
config.config,
|
||||
connection.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private _itemDisplayChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
@@ -22,10 +27,17 @@ declare global {
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() @consumeLocalize() private _localize?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -80,7 +92,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
const localize = this.localize || this._localize!;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
@@ -95,8 +107,8 @@ export class HaFileUpload extends LitElement {
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${this._locale &&
|
||||
blankBeforePercent(this._locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
@@ -24,7 +32,24 @@ interface HaFilterDevicesItem extends HaListVirtualizedItem {
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
@state()
|
||||
private _devicesReg!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -75,7 +100,7 @@ export class HaFilterDevices extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
${this._localize("ui.panel.config.devices.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -95,7 +120,13 @@ export class HaFilterDevices extends LitElement {
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(this.hass.devices, this._filter || "")}
|
||||
.rows=${this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
@@ -121,13 +152,24 @@ export class HaFilterDevices extends LitElement {
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
this.value = [
|
||||
...(this.value ?? []),
|
||||
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
|
||||
this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
|
||||
.id;
|
||||
const id = this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id;
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
}
|
||||
|
||||
@@ -153,27 +195,24 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
filter: string,
|
||||
localize: LocalizeFunc,
|
||||
states: ContextType<typeof statesContext>,
|
||||
language: string | undefined
|
||||
): HaFilterDevicesItem[] => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.map((device) => ({
|
||||
id: device.id,
|
||||
interactive: true,
|
||||
name: computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
name: computeDeviceNameDisplay(device, localize, states),
|
||||
}))
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!filter || name.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -194,7 +233,7 @@ export class HaFilterDevices extends LitElement {
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
relatedPromises.push(findRelated(this._api, "device", deviceId));
|
||||
}
|
||||
}
|
||||
const results = await Promise.all(relatedPromises);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -5,12 +6,14 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, statesContext } from "../data/context";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
@@ -20,7 +23,17 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -43,7 +56,7 @@ export class HaFilterDomains extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.domains.caption")}
|
||||
${this._localize("ui.panel.config.domains.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -65,7 +78,13 @@ export class HaFilterDomains extends LitElement {
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(this.hass.states, this._filter, this.value),
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
@@ -78,7 +97,7 @@ export class HaFilterDomains extends LitElement {
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
@@ -87,26 +106,34 @@ export class HaFilterDomains extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne((states, filter, _value) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
private _domains = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
localize: LocalizeFunc,
|
||||
language: string | undefined,
|
||||
filter: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
|
||||
.map((entry) => entry.domain);
|
||||
});
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.map((entry) => entry.domain);
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
@@ -129,7 +156,13 @@ export class HaFilterDomains extends LitElement {
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const domains = this._domains(this.hass.states, this._filter, this.value);
|
||||
const domains = this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
|
||||
const visibleDomains = new Set(domains);
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
@@ -22,7 +29,20 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -62,7 +82,7 @@ export class HaFilterEntities extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
${this._localize("ui.panel.config.entities.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -82,9 +102,10 @@ export class HaFilterEntities extends LitElement {
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
this._states,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
@@ -163,9 +184,10 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
private _entities = memoizeOne(
|
||||
(
|
||||
states: HomeAssistant["states"],
|
||||
states: ContextType<typeof statesContext>,
|
||||
type: this["type"],
|
||||
filter: string,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const values = Object.values(states);
|
||||
@@ -180,11 +202,7 @@ export class HaFilterEntities extends LitElement {
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeStateName(a),
|
||||
computeStateName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(computeStateName(a), computeStateName(b), language)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -203,7 +221,7 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
for (const entityId of this.value) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||
relatedPromises.push(findRelated(this._api, "entity", entityId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -5,14 +6,21 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
@@ -26,7 +34,24 @@ import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areasReg!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floorsReg!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
floors?: string[];
|
||||
@@ -55,7 +80,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||
const areas = this._areas(this._areasReg, this._floorsReg);
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -65,7 +90,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||
${this._localize("ui.panel.config.areas.caption")}
|
||||
${this.value?.areas?.length || this.value?.floors?.length
|
||||
? html`<div class="badge">
|
||||
${(this.value?.areas?.length || 0) +
|
||||
@@ -85,9 +110,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
multi
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
aria-label=${this._localize("ui.panel.config.areas.caption")}
|
||||
>
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
@@ -141,8 +164,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
@@ -225,7 +248,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||
(
|
||||
areaReg: ContextType<typeof areasContext>,
|
||||
floorReg: ContextType<typeof floorsContext>
|
||||
) => {
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
@@ -261,7 +287,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.areas) {
|
||||
for (const areaId of this.value.areas) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||
relatedPromises.push(findRelated(this._api, "area", areaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +295,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.floors) {
|
||||
for (const floorId of this.value.floors) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||
relatedPromises.push(findRelated(this._api, "floor", floorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -6,13 +6,14 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -25,14 +26,20 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
@@ -45,7 +52,12 @@ export class HaFilterLabels extends LitElement {
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
filter: string | undefined,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) =>
|
||||
labels
|
||||
.filter(
|
||||
(label) =>
|
||||
@@ -54,11 +66,7 @@ export class HaFilterLabels extends LitElement {
|
||||
label.label_id.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
a.name || a.label_id,
|
||||
b.name || b.label_id,
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(a.name || a.label_id, b.name || b.label_id, language)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -71,7 +79,7 @@ export class HaFilterLabels extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||
${this._localize("ui.panel.config.labels.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -96,6 +104,7 @@ export class HaFilterLabels extends LitElement {
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
@@ -129,7 +138,7 @@ export class HaFilterLabels extends LitElement {
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
`;
|
||||
@@ -169,6 +178,7 @@ export class HaFilterLabels extends LitElement {
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -14,8 +13,6 @@ import "./ha-list";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -8,7 +8,6 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -22,8 +21,6 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@@ -78,7 +75,6 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type HlsType from "hls.js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { fetchStreamUrl } from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, configContext, connectionContext } from "../data/context";
|
||||
import "./ha-alert";
|
||||
|
||||
type HlsLite = Omit<
|
||||
@@ -17,7 +20,21 @@ type HlsLite = Omit<
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@@ -140,7 +157,7 @@ class HaHLSPlayer extends LitElement {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
|
||||
if (!isComponentLoaded(this.hass.config, "stream")) {
|
||||
if (!isComponentLoaded(this._config.config, "stream")) {
|
||||
this._setFatalError("Streaming component is not loaded.");
|
||||
return;
|
||||
}
|
||||
@@ -149,9 +166,12 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(this.hass!, this.entityid);
|
||||
const { url } = await fetchStreamUrl(
|
||||
{ callWS: this._api.callWS, hassUrl: this._connection.hassUrl },
|
||||
this.entityid
|
||||
);
|
||||
|
||||
this._url = this.hass.hassUrl(url);
|
||||
this._url = this._connection.hassUrl(url);
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
@@ -184,13 +204,13 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._setFatalError(
|
||||
this.hass.localize("ui.components.media-browser.video_not_supported")
|
||||
this._localize("ui.components.media-browser.video_not_supported")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const useExoPlayer =
|
||||
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
|
||||
this.allowExoPlayer && this._config.auth.external?.config.hasExoPlayer;
|
||||
const masterPlaylist = await (await masterPlaylistPromise).text();
|
||||
|
||||
if (!this.isConnected) {
|
||||
@@ -236,7 +256,7 @@ class HaHLSPlayer extends LitElement {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.fireMessage({
|
||||
await this._config.auth.external!.fireMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: {
|
||||
url,
|
||||
@@ -250,7 +270,7 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
@@ -362,7 +382,7 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
if (this._exoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
this._config.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
this._exoPlayer = false;
|
||||
}
|
||||
if (this._videoEl) {
|
||||
|
||||
@@ -101,15 +101,9 @@ export class HaLabelsPicker extends LitElement {
|
||||
language: string
|
||||
) =>
|
||||
value
|
||||
?.map(
|
||||
(id) =>
|
||||
labels?.find((label) => label.label_id === id) || {
|
||||
label_id: id,
|
||||
name: id,
|
||||
color: "rgba(var(--rgb-primary-text-color), 0.15)",
|
||||
}
|
||||
)
|
||||
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
|
||||
?.map((id) => labels?.find((label) => label.label_id === id))
|
||||
.filter((label): label is LabelRegistryEntry => label !== undefined)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.map((label) => ({
|
||||
...label,
|
||||
style: getLabelColorStyle(label.color),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext, formattersContext } from "../data/context";
|
||||
import "./ha-button";
|
||||
import type { LawnMowerEntity, LawnMowerEntityState } from "../data/lawn_mower";
|
||||
import { LawnMowerEntityFeature } from "../data/lawn_mower";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface LawnMowerAction {
|
||||
action: string;
|
||||
@@ -39,13 +42,19 @@ const LAWN_MOWER_ACTIONS: Partial<
|
||||
|
||||
@customElement("ha-lawn_mower-action-button")
|
||||
class HaLawnMowerActionButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
|
||||
|
||||
public render() {
|
||||
const state = this.stateObj.state;
|
||||
const action = LAWN_MOWER_ACTIONS[state];
|
||||
const action = LAWN_MOWER_ACTIONS[this.stateObj.state];
|
||||
|
||||
if (action && supportsFeature(this.stateObj, action.feature)) {
|
||||
return html`
|
||||
@@ -55,14 +64,14 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
.service=${action.service}
|
||||
size="s"
|
||||
>
|
||||
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
${this._localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-button appearance="plain" disabled>
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
@@ -71,7 +80,7 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = ev.target.service;
|
||||
this.hass.callService("lawn_mower", service, {
|
||||
this._api.callService("lawn_mower", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,7 +243,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
items = multiTermSortedSearch(
|
||||
items,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -492,7 +492,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
|
||||
@@ -78,7 +78,6 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -8,9 +9,11 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
// WebAssembly port of ZXing:
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { configContext } from "../data/context";
|
||||
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
@@ -33,7 +36,13 @@ prepareZXingModule({
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -106,7 +115,7 @@ class HaQrScanner extends LitElement {
|
||||
${this._error || this._warning}
|
||||
${this._error
|
||||
? html`<ha-button @click=${this._retry} slot="action">
|
||||
${this.hass.localize("ui.components.qr-scanner.retry")}
|
||||
${this._localize("ui.components.qr-scanner.retry")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
@@ -126,7 +135,7 @@ class HaQrScanner extends LitElement {
|
||||
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.components.qr-scanner.select_camera"
|
||||
)}
|
||||
.path=${mdiCamera}
|
||||
@@ -146,28 +155,24 @@ class HaQrScanner extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.hass.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.hass.localize("ui.components.qr-scanner.not_supported")}
|
||||
? this._localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this._localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<p>${this._localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
.label=${this._localize("ui.components.qr-scanner.enter_qr_code")}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-input>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
${this._localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
private get _nativeBarcodeScanner(): boolean {
|
||||
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
|
||||
return Boolean(this._config.auth.external?.config.hasBarCodeScanner);
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
@@ -182,7 +187,7 @@ class HaQrScanner extends LitElement {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._reportError(
|
||||
this.hass.localize("ui.components.qr-scanner.no_camera_found")
|
||||
this._localize("ui.components.qr-scanner.no_camera_found")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -270,7 +275,7 @@ class HaQrScanner extends LitElement {
|
||||
if (msg.command === "bar_code/scan_result") {
|
||||
if (msg.payload.format !== "qr_code") {
|
||||
this._notifyExternalScanner(
|
||||
this.hass.localize("ui.components.qr-scanner.wrong_code", {
|
||||
this._localize("ui.components.qr-scanner.wrong_code", {
|
||||
format: msg.payload.format,
|
||||
rawValue: msg.payload.rawValue,
|
||||
})
|
||||
@@ -288,20 +293,17 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/scan",
|
||||
payload: {
|
||||
title:
|
||||
this.title ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.title"),
|
||||
this.title || this._localize("ui.components.qr-scanner.app.title"),
|
||||
description:
|
||||
this.description ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.description"),
|
||||
this._localize("ui.components.qr-scanner.app.description"),
|
||||
alternative_option_label:
|
||||
this.alternativeOptionLabel ||
|
||||
this.hass.localize(
|
||||
"ui.components.qr-scanner.app.alternativeOptionLabel"
|
||||
),
|
||||
this._localize("ui.components.qr-scanner.app.alternativeOptionLabel"),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -309,7 +311,7 @@ class HaQrScanner extends LitElement {
|
||||
private _closeExternalScanner() {
|
||||
this._removeListener?.();
|
||||
this._removeListener = undefined;
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/close",
|
||||
});
|
||||
}
|
||||
@@ -318,7 +320,7 @@ class HaQrScanner extends LitElement {
|
||||
if (!this._nativeBarcodeScanner) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/notify",
|
||||
payload: {
|
||||
message,
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { HomeAssistantInternationalization } from "../types";
|
||||
class HaRelativeTime extends ReactiveElement {
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@property() public format: Intl.RelativeTimeFormatStyle = "long";
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
@state()
|
||||
@@ -36,13 +38,15 @@ class HaRelativeTime extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
super.update(changedProps);
|
||||
if (changedProps.has("datetime")) {
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
@@ -66,15 +70,23 @@ class HaRelativeTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
|
||||
this.textContent = this._i18n.localize(
|
||||
"ui.components.relative_time.never"
|
||||
);
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this._i18n.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
const relTime = relativeTime(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
undefined,
|
||||
true,
|
||||
this.format
|
||||
);
|
||||
this.textContent = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export class HaAreasDisplaySelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-areas-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labeled-slider";
|
||||
import { generateColorTemperatureGradient } from "../../dialogs/more-info/components/lights/light-color-temp-picker";
|
||||
import {
|
||||
@@ -15,8 +14,6 @@ import {
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
export class HaColorTempSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorTempSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@@ -86,7 +86,10 @@ export class HaDateTimeSelector extends LitElement {
|
||||
static styles = css`
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Align the input fields by their top edge so the date field's underline
|
||||
lines up with the time field, since ha-date-input reserves extra space
|
||||
below for its hint while ha-time-input does not. */
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export class HaFileSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.accept=${this.selector.file?.accept}
|
||||
.icon=${mdiFile}
|
||||
.label=${this.label}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../../common/controllers/async-value-task";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { entityIcon } from "../../data/icons";
|
||||
import type { IconSelector } from "../../data/selector";
|
||||
@@ -28,23 +30,45 @@ export class HaIconSelector extends LitElement {
|
||||
icon_entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
private get _stateObj(): HassEntity | undefined {
|
||||
const iconEntity = this.context?.icon_entity;
|
||||
return iconEntity ? this.hass.states[iconEntity] : undefined;
|
||||
}
|
||||
|
||||
const stateObj = iconEntity ? this.hass.states[iconEntity] : undefined;
|
||||
private _placeholderTask = new AsyncValueTask(this, {
|
||||
task: ([
|
||||
placeholder,
|
||||
attributeIcon,
|
||||
entities,
|
||||
config,
|
||||
connection,
|
||||
stateObj,
|
||||
]) => {
|
||||
if (placeholder || attributeIcon || !stateObj) {
|
||||
return initialState;
|
||||
}
|
||||
return entityIcon(entities, config, connection, stateObj);
|
||||
},
|
||||
args: () => {
|
||||
const stateObj = this._stateObj;
|
||||
return [
|
||||
this.selector.icon?.placeholder,
|
||||
stateObj?.attributes.icon,
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj,
|
||||
] as const;
|
||||
},
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
const placeholder =
|
||||
this.selector.icon?.placeholder ||
|
||||
stateObj?.attributes.icon ||
|
||||
(stateObj &&
|
||||
until(
|
||||
entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj
|
||||
)
|
||||
));
|
||||
(stateObj && this._placeholderTask.value);
|
||||
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
|
||||
@@ -84,7 +84,6 @@ export class HaLocationSelector extends LitElement {
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -405,11 +406,12 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
let groupItems: SerialPickerItem[] = grouped[type];
|
||||
if (searchString) {
|
||||
const fuseIndex = Fuse.createIndex(DEFAULT_SEARCH_KEYS, groupItems);
|
||||
groupItems = multiTermSortedSearch(
|
||||
groupItems,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
if (!groupItems.length) {
|
||||
|
||||
@@ -3,15 +3,13 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../ha-textarea";
|
||||
import "../input/ha-input";
|
||||
import "../input/ha-input-multi";
|
||||
|
||||
@customElement("ha-selector-text")
|
||||
export class HaTextSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@@ -23,7 +23,6 @@ export class HaThemeSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-time-format-picker";
|
||||
|
||||
@customElement("ha-selector-ui_time_format")
|
||||
export class HaSelectorUiTimeFormat extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-format-picker
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-time-format-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ const LOAD_ELEMENTS = {
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
ui_time_format: () => import("./ha-selector-ui-time-format"),
|
||||
};
|
||||
|
||||
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
@@ -34,6 +35,17 @@ export class HaServiceIcon extends LitElement {
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([icon, connection, config, service]) => {
|
||||
if (icon || !connection || !config || !service) {
|
||||
return initialState;
|
||||
}
|
||||
return serviceIcon(connection, config, service);
|
||||
},
|
||||
args: () =>
|
||||
[this.icon, this._connection, this._config, this.service] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -47,16 +59,12 @@ export class HaServiceIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(this._connection, this._config, this.service).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import { serviceSectionIcon } from "../data/icons";
|
||||
@@ -31,6 +32,23 @@ export class HaServiceSectionIcon extends LitElement {
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([icon, connection, config, service, section]) => {
|
||||
if (icon || !connection || !config || !service || !section) {
|
||||
return initialState;
|
||||
}
|
||||
return serviceSectionIcon(connection, config, service, section);
|
||||
},
|
||||
args: () =>
|
||||
[
|
||||
this.icon,
|
||||
this._connection,
|
||||
this._config,
|
||||
this.service,
|
||||
this.section,
|
||||
] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -44,19 +62,12 @@ export class HaServiceSectionIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceSectionIcon(
|
||||
this._connection,
|
||||
this._config,
|
||||
this.service,
|
||||
this.section
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import {
|
||||
configContext,
|
||||
@@ -37,11 +38,47 @@ export class HaStateIcon extends LitElement {
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
protected _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
const overrideIcon =
|
||||
private get _overrideIcon(): string | undefined {
|
||||
return (
|
||||
this.icon ||
|
||||
(this.stateObj && this._entities?.[this.stateObj.entity_id]?.icon) ||
|
||||
this.stateObj?.attributes.icon;
|
||||
this.stateObj?.attributes.icon
|
||||
);
|
||||
}
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([
|
||||
overrideIcon,
|
||||
entities,
|
||||
config,
|
||||
connection,
|
||||
stateObj,
|
||||
stateValue,
|
||||
]) => {
|
||||
if (overrideIcon || !entities || !config || !connection || !stateObj) {
|
||||
return initialState;
|
||||
}
|
||||
return entityIcon(
|
||||
entities,
|
||||
config.config,
|
||||
connection.connection,
|
||||
stateObj,
|
||||
stateValue
|
||||
);
|
||||
},
|
||||
args: () =>
|
||||
[
|
||||
this._overrideIcon,
|
||||
this._entities,
|
||||
this._config,
|
||||
this._connection,
|
||||
this.stateObj,
|
||||
this.stateValue,
|
||||
] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const overrideIcon = this._overrideIcon;
|
||||
if (overrideIcon) {
|
||||
return html`<ha-icon .icon=${overrideIcon}></ha-icon>`;
|
||||
}
|
||||
@@ -51,19 +88,12 @@ export class HaStateIcon extends LitElement {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
const icon = entityIcon(
|
||||
this._entities,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this.stateObj,
|
||||
this.stateValue
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -1116,7 +1116,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -1233,7 +1232,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
@@ -23,7 +25,13 @@ export class HaThemePicker extends LitElement {
|
||||
@property({ attribute: "include-default", type: Boolean })
|
||||
public includeDefault = false;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@@ -56,8 +64,8 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getThemeOptions(
|
||||
this.hass?.themes.themes || {},
|
||||
this.hass?.locale.language || "en",
|
||||
this._ui?.themes.themes || {},
|
||||
this._i18n?.locale.language || "en",
|
||||
this.includeDefault
|
||||
);
|
||||
|
||||
@@ -70,10 +78,10 @@ export class HaThemePicker extends LitElement {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.label=${this.label ??
|
||||
this.hass?.localize("ui.components.theme-picker.theme") ??
|
||||
this._i18n?.localize("ui.components.theme-picker.theme") ??
|
||||
"Theme"}
|
||||
.placeholder=${this.noThemeLabel ??
|
||||
this.hass?.localize("ui.components.theme-picker.no_theme")}
|
||||
this._i18n?.localize("ui.components.theme-picker.no_theme")}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
|
||||
@@ -73,7 +73,6 @@ export class HaThemeSettings extends LitElement {
|
||||
${this.showThemePicker
|
||||
? html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "./ha-select";
|
||||
import type { TimestampRenderingFormat } from "../panels/lovelace/components/types";
|
||||
import { TIMESTAMP_RENDERING_FORMATS } from "../panels/lovelace/components/types";
|
||||
|
||||
@customElement("ha-time-format-picker")
|
||||
export class HaTimeFormatPicker extends LitElement {
|
||||
@property() public value?: TimestampRenderingFormat;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
private _options = memoizeOne((localize: LocalizeFunc) =>
|
||||
[{ label: localize("ui.common.auto"), value: "auto" }].concat(
|
||||
TIMESTAMP_RENDERING_FORMATS.map((format) => ({
|
||||
label:
|
||||
localize(`ui.components.time-format-picker.formats.${format}`) ||
|
||||
format,
|
||||
value: format,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
private _styleOptions = memoizeOne((localize: LocalizeFunc) => [
|
||||
{ label: localize("ui.common.auto"), value: "auto" },
|
||||
{
|
||||
label: localize("ui.components.time-format-picker.styles.short"),
|
||||
value: "short",
|
||||
},
|
||||
{
|
||||
label: localize("ui.components.time-format-picker.styles.long"),
|
||||
value: "long",
|
||||
},
|
||||
]);
|
||||
|
||||
protected render() {
|
||||
const type = typeof this.value === "object" ? this.value.type : this.value;
|
||||
const style = typeof this.value === "object" ? this.value.style : undefined;
|
||||
return html`
|
||||
<div class="row">
|
||||
<ha-select
|
||||
.label=${this.label ?? ""}
|
||||
.value=${type || "auto"}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._selectChanged}
|
||||
.options=${this._options(this._localize)}
|
||||
>
|
||||
</ha-select>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this._localize(
|
||||
"ui.components.time-format-picker.style"
|
||||
)}
|
||||
.value=${style || "auto"}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._styleChanged}
|
||||
.options=${this._styleOptions(this._localize)}
|
||||
>
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail?.value === "auto" && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.value && typeof this.value === "object" && this.value.style) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
type: ev.detail.value,
|
||||
style: this.value.style,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _styleChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const type = typeof this.value === "object" ? this.value.type : this.value;
|
||||
if (ev.detail?.value === "auto") {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
type: type,
|
||||
style: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-time-format-picker": HaTimeFormatPicker;
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,40 @@ const SEARCH_KEYS = [
|
||||
{ name: "secondary", weight: 8 },
|
||||
];
|
||||
|
||||
export const getTimezoneOptions = (): PickerComboBoxItem[] =>
|
||||
Object.entries(timezones as Record<string, string>).map(([key, value]) => ({
|
||||
id: key,
|
||||
primary: value,
|
||||
secondary: key,
|
||||
}));
|
||||
// google-timezones-json is missing the bare "UTC" and "Etc/UTC" zones, even
|
||||
// though both are valid IANA identifiers and common server defaults. Without
|
||||
// them a "UTC" configuration shows up as an unknown time zone. Add them back.
|
||||
const ADDITIONAL_TIMEZONES: PickerComboBoxItem[] = [
|
||||
{ id: "UTC", primary: "(GMT+00:00) UTC", secondary: "UTC" },
|
||||
{ id: "Etc/UTC", primary: "(GMT+00:00) UTC", secondary: "Etc/UTC" },
|
||||
];
|
||||
|
||||
// google-timezones-json also ships an invalid IANA identifier. Correct it so
|
||||
// the zone can be selected (the backend rejects the invalid id).
|
||||
const TIMEZONE_ID_CORRECTIONS: Record<string, string> = {
|
||||
"Asia/Yuzhno-Sakhalinsk": "Asia/Sakhalin",
|
||||
};
|
||||
|
||||
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
|
||||
const options: PickerComboBoxItem[] = Object.entries(
|
||||
timezones as Record<string, string>
|
||||
).map(([key, value]) => {
|
||||
const id = TIMEZONE_ID_CORRECTIONS[key] ?? key;
|
||||
return {
|
||||
id,
|
||||
primary: value,
|
||||
secondary: id,
|
||||
};
|
||||
});
|
||||
|
||||
for (const timezone of ADDITIONAL_TIMEZONES) {
|
||||
if (!options.some((option) => option.id === timezone.id)) {
|
||||
options.push(timezone);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
@customElement("ha-timezone-picker")
|
||||
export class HaTimeZonePicker extends LitElement {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiCheckCircleOutline,
|
||||
mdiChevronDown,
|
||||
mdiCircleOffOutline,
|
||||
mdiHelpCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
@@ -84,6 +85,11 @@ class HaTracePicker extends LitElement {
|
||||
"ui.panel.config.automation.trace.picker.debugged"
|
||||
);
|
||||
item.icon_path = mdiProgressWrench;
|
||||
} else if (trace.not_triggered) {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.not_triggered"
|
||||
);
|
||||
item.icon_path = mdiCircleOffOutline;
|
||||
} else if (trace.script_execution === "finished") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.finished",
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../common/controllers/async-value-task";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
@@ -71,6 +72,17 @@ export class HaTriggerIcon extends LitElement {
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
private _iconTask = new AsyncValueTask(this, {
|
||||
task: ([icon, connection, config, trigger]) => {
|
||||
if (icon || !connection || !config || !trigger) {
|
||||
return initialState;
|
||||
}
|
||||
return triggerIcon(connection, config, trigger);
|
||||
},
|
||||
args: () =>
|
||||
[this.icon, this._connection, this._config, this.trigger] as const,
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -84,16 +96,12 @@ export class HaTriggerIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = triggerIcon(this._connection, this._config, this.trigger).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
if (!this._iconTask.resolved) {
|
||||
return nothing;
|
||||
}
|
||||
return this._iconTask.value
|
||||
? html`<ha-icon .icon=${this._iconTask.value}></ha-icon>`
|
||||
: this._renderFallback();
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
|
||||
const STATES_INTERCEPTABLE: Record<
|
||||
@@ -46,7 +49,10 @@ const STATES_INTERCEPTABLE: Record<
|
||||
|
||||
@customElement("ha-vacuum-state")
|
||||
export class HaVacuumState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -68,19 +74,19 @@ export class HaVacuumState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeInterceptable(
|
||||
state: string,
|
||||
stateString: string,
|
||||
supportedFeatures: number | undefined
|
||||
) {
|
||||
return state in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
return stateString in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
}
|
||||
|
||||
private _computeLabel(state: string, interceptable: boolean) {
|
||||
private _computeLabel(stateString: string, interceptable: boolean) {
|
||||
return interceptable
|
||||
? this.hass.localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`
|
||||
? this._localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[stateString].action}`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`component.vacuum.entity_component._.state.${state}`
|
||||
: this._localize(
|
||||
`component.vacuum.entity_component._.state.${stateString}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ export class HaVacuumState extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = STATES_INTERCEPTABLE[stateObj.state].service;
|
||||
await this.hass.callService("vacuum", service, {
|
||||
await this._api.callService("vacuum", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import {
|
||||
configContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../types";
|
||||
|
||||
@customElement("ha-water_heater-state")
|
||||
export class HaWaterHeaterState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: HomeAssistantConfig;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -16,17 +41,16 @@ export class HaWaterHeaterState extends LitElement {
|
||||
return html`
|
||||
<div class="target">
|
||||
<span class="state-label label">
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
</span>
|
||||
<span class="label"
|
||||
>${this._computeTarget(this.hass, this.stateObj)}</span
|
||||
>
|
||||
<span class="label">${this._computeTarget()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeTarget(hass: HomeAssistant, stateObj: HassEntity) {
|
||||
if (!hass || !stateObj) return null;
|
||||
private _computeTarget() {
|
||||
if (!this._locale || !this._hassConfig || !this.stateObj) return null;
|
||||
const stateObj = this.stateObj;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
|
||||
if (
|
||||
@@ -35,17 +59,17 @@ export class HaWaterHeaterState extends LitElement {
|
||||
) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.target_temp_low,
|
||||
this.hass.locale
|
||||
this._locale
|
||||
)} – ${formatNumber(
|
||||
stateObj.attributes.target_temp_high,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
webRtcOffer,
|
||||
type WebRtcOfferEvent,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, connectionContext } from "../data/context";
|
||||
import "./ha-alert";
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,13 @@ import "./ha-alert";
|
||||
*/
|
||||
@customElement("ha-web-rtc-player")
|
||||
class HaWebRtcPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@@ -130,7 +137,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hass || !this.entityid) {
|
||||
if (!this._api || !this._connection || !this.entityid) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +148,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._logEvent("start clientConfig");
|
||||
|
||||
this._clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
@@ -230,8 +237,11 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._logEvent("start webRtcOffer", offer_sdp);
|
||||
|
||||
try {
|
||||
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
|
||||
this._handleOfferEvent(event)
|
||||
this._unsub = webRtcOffer(
|
||||
this._connection,
|
||||
this.entityid,
|
||||
offer_sdp,
|
||||
(event) => this._handleOfferEvent(event)
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
@@ -257,7 +267,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._sessionId = event.session_id;
|
||||
this._candidatesList.forEach((candidate) =>
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid!,
|
||||
event.session_id,
|
||||
// toJSON returns RTCIceCandidateInit
|
||||
@@ -310,7 +320,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
if (this._sessionId) {
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid,
|
||||
this._sessionId,
|
||||
// toJSON returns RTCIceCandidateInit
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("ha-entity-marker")
|
||||
class HaEntityMarker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
||||
|
||||
@property({ attribute: "entity-unit" }) public entityUnit?: string;
|
||||
@@ -36,9 +39,7 @@ class HaEntityMarker extends LitElement {
|
||||
})}
|
||||
></div>`
|
||||
: this.showIcon && this.entityId
|
||||
? html`<ha-state-icon
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
? html`<ha-state-icon .stateObj=${this._stateObj}></ha-state-icon>`
|
||||
: !this.entityUnit
|
||||
? this.entityName
|
||||
: html`
|
||||
|
||||
@@ -13,7 +13,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import type { ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
@@ -45,8 +45,6 @@ export interface MarkerLocation {
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public locations?: MarkerLocation[];
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -128,7 +126,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { isToday } from "date-fns";
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
formatTimeWeekday,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
|
||||
@@ -26,7 +29,22 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityLocation } from "../../common/entity/get_entity_location";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../data/context";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantUI,
|
||||
ThemeMode,
|
||||
} from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
@@ -76,7 +94,32 @@ export interface HaMapEntity {
|
||||
|
||||
@customElement("ha-map")
|
||||
export class HaMap extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HassEntities;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config!: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui!: HomeAssistantUI;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: HomeAssistantConnection;
|
||||
|
||||
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
|
||||
|
||||
@@ -175,17 +218,16 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
let autoFitRequired = false;
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldStates = changedProps.get("_states") as HassEntities | undefined;
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
} else if (this._loaded && oldHass && this.entities) {
|
||||
} else if (this._loaded && oldStates && this.entities) {
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
if (
|
||||
oldHass.states[getEntityId(entity)] !==
|
||||
this.hass!.states[getEntityId(entity)]
|
||||
oldStates[getEntityId(entity)] !== this._states[getEntityId(entity)]
|
||||
) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
@@ -219,10 +261,11 @@ export class HaMap extends ReactiveElement {
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
|
||||
if (
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
(!changedProps.has("_ui") ||
|
||||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -233,7 +276,7 @@ export class HaMap extends ReactiveElement {
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
|
||||
(this.themeMode === "auto" && Boolean(this._ui?.themes.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,8 +301,8 @@ export class HaMap extends ReactiveElement {
|
||||
this._loading = true;
|
||||
try {
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
|
||||
latitude: this.hass?.config.latitude ?? 52.3731339,
|
||||
longitude: this.hass?.config.longitude ?? 4.8903147,
|
||||
latitude: this._config?.latitude ?? 52.3731339,
|
||||
longitude: this._config?.longitude ?? 4.8903147,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
this._updateMapStyle();
|
||||
@@ -300,7 +343,7 @@ export class HaMap extends ReactiveElement {
|
||||
if (options?.unpause_autofit) {
|
||||
this._pauseAutoFit = false;
|
||||
}
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
if (!this.leafletMap || !this.Leaflet || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,10 +354,7 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
new this.Leaflet.LatLng(this._config.latitude, this._config.longitude),
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
setTimeout(() => {
|
||||
@@ -351,7 +391,7 @@ export class HaMap extends ReactiveElement {
|
||||
boundingbox: LatLngExpression[],
|
||||
options?: { zoom?: number; pad?: number }
|
||||
) {
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
if (!this.leafletMap || !this.Leaflet) {
|
||||
return;
|
||||
}
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
@@ -382,32 +422,31 @@ export class HaMap extends ReactiveElement {
|
||||
if (path.fullDatetime) {
|
||||
formattedTime = formatDateTime(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
} else if (isToday(point.timestamp)) {
|
||||
formattedTime = formatTimeWithSeconds(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
} else {
|
||||
formattedTime = formatTimeWeekday(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
}
|
||||
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
|
||||
}
|
||||
|
||||
private _drawPaths(): void {
|
||||
const hass = this.hass;
|
||||
const map = this.leafletMap;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Leaflet = this.Leaflet;
|
||||
|
||||
if (!hass || !map || !Leaflet) {
|
||||
if (!this._i18n || !this._config || !map || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
if (this._mapPaths.length) {
|
||||
@@ -535,12 +574,12 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _drawEntities(): void {
|
||||
const hass = this.hass;
|
||||
const states = this._states;
|
||||
const map = this.leafletMap;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Leaflet = this.Leaflet;
|
||||
|
||||
if (!hass || !map || !Leaflet) {
|
||||
if (!states || !map || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,7 +617,7 @@ export class HaMap extends ReactiveElement {
|
||||
const className = this._darkMode ? "dark" : "light";
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = hass.states[getEntityId(entity)];
|
||||
const stateObj = states[getEntityId(entity)];
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
@@ -591,7 +630,7 @@ export class HaMap extends ReactiveElement {
|
||||
entity_picture: entityPicture,
|
||||
} = stateObj.attributes;
|
||||
|
||||
const location = getEntityLocation(stateObj, hass.states);
|
||||
const location = getEntityLocation(stateObj, states);
|
||||
if (!location) {
|
||||
continue;
|
||||
}
|
||||
@@ -648,11 +687,14 @@ export class HaMap extends ReactiveElement {
|
||||
// create icon
|
||||
const entityName =
|
||||
typeof entity !== "string" && entity.label_mode === "state"
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
? this._formatters.formatEntityState(stateObj)
|
||||
: typeof entity !== "string" &&
|
||||
entity.label_mode === "attribute" &&
|
||||
entity.attribute !== undefined
|
||||
? this.hass.formatEntityAttributeValue(stateObj, entity.attribute)
|
||||
? this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
entity.attribute
|
||||
)
|
||||
: (customTitle ??
|
||||
title
|
||||
.split(" ")
|
||||
@@ -661,7 +703,6 @@ export class HaMap extends ReactiveElement {
|
||||
.substr(0, 3));
|
||||
|
||||
const entityMarker = document.createElement("ha-entity-marker");
|
||||
entityMarker.hass = this.hass;
|
||||
entityMarker.showIcon =
|
||||
typeof entity !== "string" && entity.label_mode === "icon";
|
||||
entityMarker.entityId = getEntityId(entity);
|
||||
@@ -674,7 +715,7 @@ export class HaMap extends ReactiveElement {
|
||||
: "";
|
||||
entityMarker.entityPicture =
|
||||
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
||||
? this.hass.hassUrl(entityPicture)
|
||||
? this._connection.hassUrl(entityPicture)
|
||||
: "";
|
||||
if (typeof entity !== "string") {
|
||||
entityMarker.entityColor = entity.color;
|
||||
|
||||
@@ -100,7 +100,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
: nothing}
|
||||
<div class="content">
|
||||
<ha-media-player-toggle
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
checked
|
||||
disabled
|
||||
@@ -108,7 +107,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
${this._mediaPlayerEntities(this.hass.entities).map(
|
||||
(entity) =>
|
||||
html`<ha-media-player-toggle
|
||||
.hass=${this.hass}
|
||||
.entityId=${entity.entity_id}
|
||||
.checked=${this._selectedEntities.includes(entity.entity_id)}
|
||||
@change=${this._handleSelectedChange}
|
||||
|
||||
@@ -101,7 +101,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
</span>
|
||||
<ha-media-manage-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.currentItem=${this._currentItem}
|
||||
@media-refresh=${this._refreshMedia}
|
||||
></ha-media-manage-button>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFolderEdit } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { configContext } from "../../data/context";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
isLocalMediaSourceContentId,
|
||||
isImageUploadMediaSourceContentId,
|
||||
} from "../../data/media_source";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-button";
|
||||
import { showMediaManageDialog } from "./show-media-manage-dialog";
|
||||
@@ -20,7 +23,13 @@ declare global {
|
||||
|
||||
@customElement("ha-media-manage-button")
|
||||
class MediaManageButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) currentItem?: MediaPlayerItem;
|
||||
|
||||
@@ -31,7 +40,7 @@ class MediaManageButton extends LitElement {
|
||||
!this.currentItem ||
|
||||
!(
|
||||
isLocalMediaSourceContentId(this.currentItem.media_content_id || "") ||
|
||||
(this.hass!.user?.is_admin &&
|
||||
(this._config.user?.is_admin &&
|
||||
isImageUploadMediaSourceContentId(this.currentItem.media_content_id))
|
||||
)
|
||||
) {
|
||||
@@ -40,9 +49,7 @@ class MediaManageButton extends LitElement {
|
||||
return html`
|
||||
<ha-button appearance="filled" size="s" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
)}
|
||||
${this._localize("ui.components.media-browser.file_management.manage")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../../data/context";
|
||||
|
||||
import "../ha-switch";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-media-player-toggle")
|
||||
class HaMediaPlayerToggle extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@property({ type: Boolean }) public checked = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
@state()
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
@state()
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
entities: ContextType<typeof entitiesContext>,
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
isRTL: boolean,
|
||||
stateObj: HomeAssistant["states"][string]
|
||||
stateObj: HassEntity
|
||||
) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
@@ -50,7 +81,11 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let icon = mdiSpeaker;
|
||||
if (stateObj.state === "playing") {
|
||||
@@ -60,16 +95,16 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors,
|
||||
isRTL,
|
||||
stateObj
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user