Compare commits

...

17 Commits

Author SHA1 Message Date
Aidan Timson 932e74fdaa Casing
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-07-01 15:55:45 +01:00
Aidan Timson df65ba0a3d Redirect old developer tools URLs on initial load and add tools e2e tests 2026-07-01 15:43:25 +01:00
Aidan Timson 61cdc8ba82 Update bug report description 2026-07-01 15:35:33 +01:00
Aidan Timson 2936614b54 Load config fragment for statistics repairs 2026-07-01 14:10:40 +01:00
Aidan Timson 627e7822cc Rename developer tools translation keys to tools 2026-07-01 14:10:31 +01:00
Aidan Timson 3b4a17cf26 Update developer tools link in issue template 2026-07-01 13:54:25 +01:00
Aidan Timson 5422614fbe Update e2e tests for the tools panel 2026-07-01 13:54:17 +01:00
Aidan Timson e1e4b29227 Rename developer tools panel to Tools 2026-07-01 13:54:17 +01:00
Aidan Timson 49e74bc374 Add tools my-link redirects 2026-07-01 13:53:46 +01:00
Aidan Timson cd539efc98 Redirect old developer tools URLs to /config/tools 2026-07-01 13:53:23 +01:00
Aidan Timson 4cf70c40c9 Point config panel routing at /config/tools 2026-07-01 13:53:15 +01:00
Aidan Timson 1bc24d9dfa Rename developer tools element tags and imports 2026-07-01 13:52:49 +01:00
Aidan Timson e84cb85c8e Rename 2026-07-01 13:49:54 +01:00
Aidan Timson 23335fffdb Migrate hui-warning and hui-error-card to lazy context (#52926) 2026-07-01 15:32:50 +03:00
Paul Bottein 0a93a681e3 Show the event type in the logbook for event entities (#52863) 2026-07-01 13:32:10 +02:00
renovate[bot] 7bc2cad83e Update dependency eslint-plugin-import-x to v4.17.1 (#52923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01 14:17:12 +03:00
Aidan Timson 39ee60a8ef Migrate all card features to lazy context (#52922)
* Migrate all card features to lazy context

* Gate render() on _locale in hui-date-set-card-feature

* fix typo: rename supportsFanOscilatteCardFeatureFromState to supportsFanOscillateCardFeatureFromState

* Apply Prettier formatting to rebased card feature templates.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 14:16:48 +03:00
89 changed files with 2024 additions and 1273 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/config/developer-tools/state`
You can find this information at `/config/tools/state`
-->
```yaml
+2 -2
View File
@@ -94,8 +94,8 @@ body:
label: State of relevant entities
description: >
If your issue is about how an entity is shown in the UI, please add the
state and attributes for all situations. You can find this information
at Developer Tools -> States.
state and attributes for all situations. You can find this
information in the Details view of the More info dialog.
render: txt
- type: textarea
attributes:
+1 -1
View File
@@ -172,7 +172,7 @@
"eslint": "10.6.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.17.0",
"eslint-plugin-import-x": "4.17.1",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
+2 -2
View File
@@ -57,8 +57,8 @@ export const CONFIG_SUB_ROUTES: Record<
translationKey: "ui.components.navigation-picker.route.scripts",
iconPath: mdiScriptText,
},
"developer-tools": {
translationKey: "ui.components.navigation-picker.route.developer_tools",
tools: {
translationKey: "ui.components.navigation-picker.route.tools",
iconPath: mdiHammer,
},
integrations: {
+16 -6
View File
@@ -27,6 +27,7 @@ export interface LogbookEntry {
source?: string; // The trigger source (English phrase, parsed for the cause)
domain?: string;
state?: string; // The state of the entity
attributes?: { event_type?: string }; // Selected attributes the backend surfaces
// Context data
context_id?: string;
context_user_id?: string;
@@ -244,13 +245,13 @@ export const parseTriggerSource = (source: string): ParsedTriggerSource => {
};
// Short label shown instead of the bare timestamp for each timestamp-state
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value), so a
// new timestamp domain won't compile until it gets a label here.
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value) and
// event (handled separately via its event type), so a new timestamp domain
// won't compile until it gets a label here.
type LogbookActionMessage =
| "pressed"
| "activated"
| "scanned"
| "detected_event_no_type"
| "updated"
| "sent"
| "detected"
@@ -261,14 +262,13 @@ type LogbookActionMessage =
| "command_sent";
const STATE_ACTION_MESSAGES: Record<
Exclude<TimestampStateDomain, "datetime">,
Exclude<TimestampStateDomain, "datetime" | "event">,
LogbookActionMessage
> = {
button: "pressed",
input_button: "pressed",
scene: "activated",
tag: "scanned",
event: "detected_event_no_type",
image: "updated",
notify: "sent",
wake_word: "detected",
@@ -284,8 +284,18 @@ export const localizeStateMessage = (
hass: HomeAssistant,
state: string,
stateObj: HassEntity,
domain: string
domain: string,
attributes?: LogbookEntry["attributes"]
): string => {
// Events show the triggered event type, falling back to a generic label when
// the type is unknown (the timestamp state is meaningless on its own).
if (domain === "event") {
const eventType = attributes?.event_type;
if (eventType != null) {
return hass.formatEntityAttributeValue(stateObj, "event_type", eventType);
}
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
}
const actionKey: LogbookActionMessage | undefined =
STATE_ACTION_MESSAGES[domain as keyof typeof STATE_ACTION_MESSAGES];
if (actionKey) {
+17 -5
View File
@@ -28,6 +28,21 @@ const useHash = __DEMO__;
const curPath = () =>
useHash ? location.hash.substring(1) : location.pathname;
// Developer tools was renamed to Tools (/config/tools) in 2026.8; it had moved
// from /developer-tools to /config in 2026.2. Redirect both old locations to
// the new one. Applied on the initial route and on every navigation so
// bookmarks and external links to the old URLs resolve too, not just in-app
// navigation.
const redirectLegacyToolsPath = (path: string): string => {
if (path.startsWith("/config/developer-tools")) {
return path.replace("/config/developer-tools", "/config/tools");
}
if (path.startsWith("/developer-tools")) {
return path.replace("/developer-tools", "/config/tools");
}
return path;
};
const panelUrl = (path: string) => {
const dividerPos = path.indexOf("/", 1);
return dividerPos === -1 ? path.substring(1) : path.substring(1, dividerPos);
@@ -50,7 +65,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
constructor() {
super();
const path = curPath();
const path = redirectLegacyToolsPath(curPath());
this._route = {
prefix: "",
@@ -106,10 +121,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
// Navigation
const updateRoute = (path = curPath()) => {
// Developer tools panel was moved to config in 2026.2
if (path.startsWith("/developer-tools")) {
path = path.replace("/developer-tools", "/config/developer-tools");
}
path = redirectLegacyToolsPath(path);
if (this._route && path === this._route.path) {
return;
}
+5 -5
View File
@@ -211,8 +211,8 @@ export const configSections: Record<string, PageNavigation[]> = {
adminOnly: true,
},
{
path: "/config/developer-tools",
translationKey: "developer_tools",
path: "/config/tools",
translationKey: "tools",
iconPath: mdiHammer,
iconColor: "#7A5AA6",
core: true,
@@ -328,10 +328,10 @@ export const configSections: Record<string, PageNavigation[]> = {
adminOnly: true,
},
],
developer_tools: [
tools: [
{
path: "/config/developer-tools",
translationKey: "ui.panel.config.dashboard.developer_tools.main",
path: "/config/tools",
translationKey: "ui.panel.config.dashboard.tools.main",
iconPath: mdiHammer,
iconColor: "#7A5AA6",
core: true,
+3 -3
View File
@@ -70,9 +70,9 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-system-navigation",
load: () => import("./core/ha-config-system-navigation"),
},
"developer-tools": {
tag: "ha-panel-developer-tools",
load: () => import("./developer-tools/ha-panel-developer-tools"),
tools: {
tag: "ha-panel-tools",
load: () => import("./tools/ha-panel-tools"),
cache: true,
},
logs: {
@@ -18,7 +18,7 @@ import {
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { fixStatisticsIssue } from "../developer-tools/statistics/fix-statistics";
import { fixStatisticsIssue } from "../tools/statistics/fix-statistics";
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
@@ -171,7 +171,7 @@ class HaConfigRepairs extends LitElement {
issue.translation_key &&
STATISTIC_TYPES.includes(issue.translation_key as any)
) {
this.hass.loadFragmentTranslation("developer-tools");
this.hass.loadFragmentTranslation("config");
const data = await fetchRepairsIssueData(
this.hass.connection,
issue.domain,
@@ -48,7 +48,7 @@ import { resolveMediaSource } from "../../../../data/media_source";
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
import { withViewTransition } from "../../../../common/util/view-transition";
@customElement("developer-tools-action")
@customElement("tools-action")
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -130,14 +130,12 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
const modeButtons: ToggleButton[] = [
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
),
label: this.hass.localize("ui.panel.config.tools.tabs.actions.ui_mode"),
value: "ui",
},
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
"ui.panel.config.tools.tabs.actions.yaml_mode"
),
value: "yaml",
},
@@ -163,7 +161,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
<div class="header-row">
<div class="header-title">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.title"
"ui.panel.config.tools.tabs.actions.title"
)}
</div>
<ha-button-toggle-group
@@ -177,7 +175,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
</div>
<p class="secondary">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
"ui.panel.config.tools.tabs.actions.description"
)}
</p>
</div>
@@ -220,14 +218,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
!this._uiAvailable
? html`<span class="error"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.no_template_ui_support"
"ui.panel.config.tools.tabs.actions.no_template_ui_support"
)}</span
>`
: nothing
}
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
"ui.panel.config.tools.tabs.actions.call_service"
)}
</ha-progress-button>
</div>
@@ -238,7 +236,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
? html`<div class="content response">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.response"
"ui.panel.config.tools.tabs.actions.response"
)}
>
<div class="card-content">
@@ -253,7 +251,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
slot="extra-actions"
@click=${this._copyTemplate}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.copy_clipboard_template"
"ui.panel.config.tools.tabs.actions.copy_clipboard_template"
)}</ha-button
>
</ha-yaml-editor>
@@ -270,10 +268,10 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
.header=${
this._yamlMode
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.all_parameters"
"ui.panel.config.tools.tabs.actions.all_parameters"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_parameters"
"ui.panel.config.tools.tabs.actions.yaml_parameters"
)
}
outlined
@@ -287,7 +285,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
target
? html`
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.accepts_target"
"ui.panel.config.tools.tabs.actions.accepts_target"
)}
`
: ""
@@ -322,17 +320,17 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
<tr>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_parameter"
"ui.panel.config.tools.tabs.actions.column_parameter"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_description"
"ui.panel.config.tools.tabs.actions.column_description"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_example"
"ui.panel.config.tools.tabs.actions.column_example"
)}
</th>
</tr>
@@ -371,7 +369,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
appearance="plain"
@click=${this._fillExampleData}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.fill_example_data"
"ui.panel.config.tools.tabs.actions.fill_example_data"
)}</ha-button
>`
: ""
@@ -406,14 +404,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
const errorCategory = yamlMode ? "yaml" : "ui";
if (!serviceData?.action) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_action`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.no_action`
);
}
const domain = computeDomain(serviceData.action);
const service = computeObjectId(serviceData.action);
if (!domain || !service) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.invalid_action`
);
}
const dataIsTemplate =
@@ -427,7 +425,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
!serviceData.data?.area_id
) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_target`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.no_target`
);
}
for (const field of fields) {
@@ -437,7 +435,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
(!serviceData.data || serviceData.data[field.key] === undefined)
) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.missing_required_field`,
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.missing_required_field`,
{ key: field.key }
);
}
@@ -496,7 +494,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
forwardHaptic(this, "failure");
button.actionError();
this._error = this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.errors.yaml.invalid_yaml"
"ui.panel.config.tools.tabs.actions.errors.yaml.invalid_yaml"
);
return;
}
@@ -569,7 +567,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
rel="noreferrer"
><ha-button>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.open_media"
"ui.panel.config.tools.tabs.actions.open_media"
)}
</ha-button></a
>
@@ -829,6 +827,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-action": HaPanelDevAction;
"tools-action": HaPanelDevAction;
}
}
@@ -25,7 +25,7 @@ interface SentenceParsingResult {
result: AssistDebugResult | null;
}
@customElement("developer-tools-assist")
@customElement("tools-assist")
class HaPanelDevAssist extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -118,14 +118,14 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.title"
"ui.panel.config.tools.tabs.assist.title"
)}
class="form"
>
<div class="card-content">
<p class="description">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.description"
"ui.panel.config.tools.tabs.assist.description"
)}
</p>
${
@@ -143,7 +143,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
<ha-textarea
resize="auto"
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.sentences"
"ui.panel.config.tools.tabs.assist.sentences"
)}
id="sentences-input"
@input=${this._textAreaInput}
@@ -157,7 +157,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
.disabled=${!this._language || !this._validInput}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.parse_sentences"
"ui.panel.config.tools.tabs.assist.parse_sentences"
)}
</ha-button>
</div>
@@ -184,7 +184,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.download_results"
"ui.panel.config.tools.tabs.assist.download_results"
)}
</ha-button>
</div>
@@ -204,7 +204,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
</div>
<div class="info">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.language"
"ui.panel.config.tools.tabs.assist.language"
)}:
${formatLanguageCode(language, this.hass.locale)}
(${language})
@@ -221,7 +221,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
`
: html`<ha-alert alert-type="error">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.no_match"
"ui.panel.config.tools.tabs.assist.no_match"
)}
</ha-alert>`
}
@@ -304,6 +304,6 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-assist": HaPanelDevAssist;
"tools-assist": HaPanelDevAssist;
}
}
@@ -17,12 +17,12 @@ class HaDebugConnectionRow extends LitElement {
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
"ui.panel.config.tools.tabs.debug.debug_connection.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.description"
"ui.panel.config.tools.tabs.debug.debug_connection.description"
)}</span
>
<ha-switch
@@ -20,12 +20,12 @@ class HaDebugDisableViewTransitionRow extends LitElement {
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
"ui.panel.config.tools.tabs.debug.disable_view_transition.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description"
"ui.panel.config.tools.tabs.debug.disable_view_transition.description"
)}</span
>
<ha-switch
@@ -230,13 +230,13 @@ export class HaDebugViewportEnvironmentCard extends LitElement {
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.viewport_environment.title"
"ui.panel.config.tools.tabs.debug.viewport_environment.title"
)}
>
<div class="card-content">
<p class="explanation">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.viewport_environment.description"
"ui.panel.config.tools.tabs.debug.viewport_environment.description"
)}
</p>
<ha-code-editor
@@ -20,7 +20,7 @@ import "./ha-debug-connection-row";
import "./ha-debug-disable-view-transition-row";
import "./ha-debug-viewport-environment-card";
@customElement("developer-tools-debug")
@customElement("tools-debug")
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -31,7 +31,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
"ui.panel.config.tools.tabs.debug.title"
)}
>
<ha-list-base>
@@ -45,13 +45,13 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.title"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.title"
)}
>
<div class="card-content">
<ha-entity-picker
.helper=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.description"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.description"
)}
@value-changed=${this._entityPicked}
></ha-entity-picker>
@@ -62,7 +62,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
appearance="filled"
.disabled=${!this._entityId}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.copy_to_clipboard"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.copy_to_clipboard"
)}</ha-button
>
</div>
@@ -136,6 +136,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-debug": HaPanelDevDebug;
"tools-debug": HaPanelDevDebug;
}
}
@@ -76,7 +76,7 @@ class EventSubscribeCard extends LitElement {
return html`
<ha-card
header=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.listen_to_events"
"ui.panel.config.tools.tabs.events.listen_to_events"
)}
>
<div class="card-content">
@@ -84,10 +84,10 @@ class EventSubscribeCard extends LitElement {
.label=${
this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.listening_to"
"ui.panel.config.tools.tabs.events.listening_to"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_to"
"ui.panel.config.tools.tabs.events.subscribe_to"
)
}
.disabled=${this._subscribed !== undefined}
@@ -96,11 +96,11 @@ class EventSubscribeCard extends LitElement {
></ha-input>
<ha-input
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.filter_events"
"ui.panel.config.tools.tabs.events.filter_events"
)}
.value=${this._eventFilter}
.disabled=${this._subscribed !== undefined}
.hint=${`${this.hass!.localize("ui.panel.config.developer-tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.config.developer-tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
.hint=${`${this.hass!.localize("ui.panel.config.tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.config.tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
@input=${this._filterChanged}
></ha-input>
${
@@ -118,10 +118,10 @@ class EventSubscribeCard extends LitElement {
${
this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.stop_listening"
"ui.panel.config.tools.tabs.events.stop_listening"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.start_listening"
"ui.panel.config.tools.tabs.events.start_listening"
)
}
</ha-button>
@@ -131,7 +131,7 @@ class EventSubscribeCard extends LitElement {
@click=${this._clearEvents}
>
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.clear_events"
"ui.panel.config.tools.tabs.events.clear_events"
)}
</ha-button>
</div>
@@ -144,10 +144,10 @@ class EventSubscribeCard extends LitElement {
if (!this._events.length) {
const message = this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.waiting_for_events"
"ui.panel.config.tools.tabs.events.waiting_for_events"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_prompt"
"ui.panel.config.tools.tabs.events.subscribe_prompt"
);
return html`
<ha-card class="events-card">
@@ -172,7 +172,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronDoubleLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.oldest_event"
"ui.panel.config.tools.tabs.events.oldest_event"
)}
@click=${this._showOldest}
></ha-icon-button>
@@ -180,13 +180,13 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.older_event"
"ui.panel.config.tools.tabs.events.older_event"
)}
@click=${this._showOlder}
></ha-icon-button>
<div class="event-info">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
"ui.panel.config.tools.tabs.events.event_fired",
{
name: position,
time: formatTimeWithSeconds(
@@ -208,7 +208,7 @@ class EventSubscribeCard extends LitElement {
<ha-tooltip for="buffer-info" placement="bottom">
<span class="buffer-tooltip">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.buffer_disclaimer",
"ui.panel.config.tools.tabs.events.buffer_disclaimer",
{ count: MAX_BUFFERED_EVENTS }
)}
</span>
@@ -221,7 +221,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newer_event"
"ui.panel.config.tools.tabs.events.newer_event"
)}
@click=${this._showNewer}
></ha-icon-button>
@@ -229,7 +229,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronDoubleRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newest_event"
"ui.panel.config.tools.tabs.events.newest_event"
)}
@click=${this._showNewest}
></ha-icon-button>
@@ -362,13 +362,13 @@ class EventSubscribeCard extends LitElement {
}, this._eventType);
} catch (error) {
this._error = this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_failed",
"ui.panel.config.tools.tabs.events.subscribe_failed",
{
error:
error instanceof Error
? error.message
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.unknown_error"
"ui.panel.config.tools.tabs.events.unknown_error"
),
}
);
@@ -27,7 +27,7 @@ class EventsList extends LitElement {
>
<span>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.count_listeners",
"ui.panel.config.tools.tabs.events.count_listeners",
{
count: event.listener_count,
}
@@ -14,7 +14,7 @@ import { documentationUrl } from "../../../../util/documentation-url";
import "./event-subscribe-card";
import "./events-list";
@customElement("developer-tools-event")
@customElement("tools-event")
class HaPanelDevEvent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -40,7 +40,7 @@ class HaPanelDevEvent extends LitElement {
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.description"
"ui.panel.config.tools.tabs.events.description"
)}
<a
href=${documentationUrl(
@@ -51,14 +51,14 @@ class HaPanelDevEvent extends LitElement {
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.documentation"
"ui.panel.config.tools.tabs.events.documentation"
)}
</a>
</p>
<div class="inputs">
<ha-input
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.type"
"ui.panel.config.tools.tabs.events.type"
)}
autofocus
required
@@ -67,7 +67,7 @@ class HaPanelDevEvent extends LitElement {
></ha-input>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.data"
"ui.panel.config.tools.tabs.events.data"
)}
</p>
</div>
@@ -85,7 +85,7 @@ class HaPanelDevEvent extends LitElement {
appearance="filled"
.disabled=${!this._isValid}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.fire_event"
"ui.panel.config.tools.tabs.events.fire_event"
)}</ha-button
>
</div>
@@ -101,7 +101,7 @@ class HaPanelDevEvent extends LitElement {
<div>
<h2>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.active_listeners"
"ui.panel.config.tools.tabs.events.active_listeners"
)}
</h2>
<events-list
@@ -131,7 +131,7 @@ class HaPanelDevEvent extends LitElement {
if (!this._eventType) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.alert_event_type"
"ui.panel.config.tools.tabs.events.alert_event_type"
),
});
return;
@@ -143,7 +143,7 @@ class HaPanelDevEvent extends LitElement {
);
fireEvent(this, "hass-notification", {
message: this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.notification_event_fired",
"ui.panel.config.tools.tabs.events.notification_event_fired",
{ type: this._eventType }
),
});
@@ -221,6 +221,6 @@ class HaPanelDevEvent extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-event": HaPanelDevEvent;
"tools-event": HaPanelDevEvent;
}
}
@@ -12,42 +12,42 @@ import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-top-app-bar-fixed";
import type { HomeAssistant, Route } from "../../../types";
import "./developer-tools-router";
import "./tools-router";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const DEVELOPER_TOOLS_TABS = [
const TOOLS_TABS = [
{
panel: "yaml",
translationKey: "ui.panel.config.developer-tools.tabs.yaml.title",
translationKey: "ui.panel.config.tools.tabs.yaml.title",
},
{
panel: "state",
translationKey: "ui.panel.config.developer-tools.tabs.states.title",
translationKey: "ui.panel.config.tools.tabs.states.title",
},
{
panel: "action",
translationKey: "ui.panel.config.developer-tools.tabs.actions.title",
translationKey: "ui.panel.config.tools.tabs.actions.title",
},
{
panel: "template",
translationKey: "ui.panel.config.developer-tools.tabs.templates.title",
translationKey: "ui.panel.config.tools.tabs.templates.title",
},
{
panel: "event",
translationKey: "ui.panel.config.developer-tools.tabs.events.title",
translationKey: "ui.panel.config.tools.tabs.events.title",
},
{
panel: "statistics",
translationKey: "ui.panel.config.developer-tools.tabs.statistics.title",
translationKey: "ui.panel.config.tools.tabs.statistics.title",
},
{
panel: "assist",
translationKey: "ui.panel.config.developer-tools.tabs.assist.tab",
translationKey: "ui.panel.config.tools.tabs.assist.tab",
},
] as const;
@customElement("ha-panel-developer-tools")
class PanelDeveloperTools extends LitElement {
@customElement("ha-panel-tools")
class PanelTools extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@@ -68,9 +68,7 @@ class PanelDeveloperTools extends LitElement {
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div slot="title">
${this.hass.localize(
"ui.panel.config.dashboard.developer_tools.main"
)}
${this.hass.localize("ui.panel.config.dashboard.tools.main")}
</div>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
@@ -79,13 +77,11 @@ class PanelDeveloperTools extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="debug">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
)}
${this.hass.localize("ui.panel.config.tools.tabs.debug.title")}
</ha-dropdown-item>
</ha-dropdown>
<ha-tab-group @wa-tab-show=${this._handlePageSelected} slot="subRow">
${DEVELOPER_TOOLS_TABS.map(
${TOOLS_TABS.map(
(tab) => html`
<ha-tab-group-tab
slot="nav"
@@ -93,7 +89,7 @@ class PanelDeveloperTools extends LitElement {
.active=${page === tab.panel}
>
<a
href="/config/developer-tools/${tab.panel}"
href="/config/tools/${tab.panel}"
@click=${this._handleTabAnchorClick}
>${this.hass.localize(tab.translationKey)}</a
>
@@ -101,11 +97,11 @@ class PanelDeveloperTools extends LitElement {
`
)}
</ha-tab-group>
<developer-tools-router
<tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
></tools-router>
</ha-top-app-bar-fixed>
`;
}
@@ -124,7 +120,7 @@ class PanelDeveloperTools extends LitElement {
return;
}
if (newPage !== this._page) {
navigate(`/config/developer-tools/${newPage}`);
navigate(`/config/tools/${newPage}`);
} else {
scrollTo({ behavior: "smooth", top: 0 });
}
@@ -133,7 +129,7 @@ class PanelDeveloperTools extends LitElement {
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
if (action === "debug") {
navigate(`/config/developer-tools/debug`);
navigate(`/config/tools/debug`);
}
}
@@ -146,7 +142,7 @@ class PanelDeveloperTools extends LitElement {
}
static readonly styles: CSSResultGroup = css`
developer-tools-router {
tools-router {
display: block;
height: 100%;
}
@@ -170,6 +166,6 @@ class PanelDeveloperTools extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-panel-developer-tools": PanelDeveloperTools;
"ha-panel-tools": PanelTools;
}
}
@@ -24,7 +24,7 @@ import { haStyle } from "../../../../resources/styles";
import { loadVirtualizer } from "../../../../resources/virtualizer";
import { showToast } from "../../../../util/toast";
@customElement("developer-tools-state-renderer")
@customElement("tools-state-renderer")
class HaPanelDevStateRenderer extends LitElement {
@property({ attribute: false }) public entities: HassEntity[] = [];
@@ -79,16 +79,12 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="row" role="row" aria-rowindex="1">
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.entity"
)}
${this._i18n.localize("ui.panel.config.tools.tabs.states.entity")}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
)}
${this._i18n.localize("ui.panel.config.tools.tabs.states.state")}
</span>
</div>
<div class="header" role="columnheader" ?hidden=${!showDevice}>
@@ -106,7 +102,7 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
"ui.panel.config.tools.tabs.states.attributes"
)}
</span>
</div>
@@ -134,7 +130,7 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="cell" role="cell" aria-colspan="5">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.no_entities"
"ui.panel.config.tools.tabs.states.no_entities"
)}
</span>
</div>
@@ -195,10 +191,10 @@ class HaPanelDevStateRenderer extends LitElement {
@click=${this._copyEntity}
.entity=${item}
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
.path=${mdiClipboardTextMultipleOutline}
></ha-svg-icon>
@@ -211,10 +207,10 @@ class HaPanelDevStateRenderer extends LitElement {
@click=${this._entityMoreInfo}
.entity=${item}
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
"ui.panel.config.tools.tabs.states.more_info"
)}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
"ui.panel.config.tools.tabs.states.more_info"
)}
.path=${mdiInformationOutline}
></ha-svg-icon>
@@ -438,7 +434,7 @@ class HaPanelDevStateRenderer extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-state-renderer": HaPanelDevStateRenderer;
"tools-state-renderer": HaPanelDevStateRenderer;
}
interface HASSDomEvents {
@@ -42,7 +42,7 @@ import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, HomeAssistantRegistries } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "./developer-tools-state-renderer";
import "./tools-state-renderer";
// Use virtualizer after threshold to avoid performance issues
// NOTE: If virtualizer is used when filtered entiity state
@@ -51,7 +51,7 @@ import "./developer-tools-state-renderer";
// virtualized list, an undesirable effect.
const VIRTUALIZE_THRESHOLD = 100;
@customElement("developer-tools-state")
@customElement("tools-state")
class HaPanelDevState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -165,7 +165,7 @@ class HaPanelDevState extends LitElement {
<div class="heading">
<h1>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.current_entities"
"ui.panel.config.tools.tabs.states.current_entities"
)}
</h1>
${
@@ -192,7 +192,7 @@ class HaPanelDevState extends LitElement {
@change=${this._saveAttributeCheckboxState}
>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
"ui.panel.config.tools.tabs.states.attributes"
)}
</ha-checkbox>
`
@@ -201,7 +201,7 @@ class HaPanelDevState extends LitElement {
</div>
<ha-expansion-panel
.header=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
"ui.panel.config.tools.tabs.states.set_state"
)}
outlined
.expanded=${this._expanded}
@@ -209,10 +209,10 @@ class HaPanelDevState extends LitElement {
>
<p>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description1"
"ui.panel.config.tools.tabs.states.description1"
)}<br />
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description2"
"ui.panel.config.tools.tabs.states.description2"
)}
</p>
${
@@ -237,7 +237,7 @@ class HaPanelDevState extends LitElement {
.path=${mdiContentCopy}
@click=${this._copyStateEntity}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
></ha-icon-button>
</div>
@@ -246,7 +246,7 @@ class HaPanelDevState extends LitElement {
}
<ha-input
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
"ui.panel.config.tools.tabs.states.state"
)}
required
autocapitalize="none"
@@ -259,7 +259,7 @@ class HaPanelDevState extends LitElement {
></ha-input>
<p>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state_attributes"
"ui.panel.config.tools.tabs.states.state_attributes"
)}
</p>
<ha-yaml-editor
@@ -274,7 +274,7 @@ class HaPanelDevState extends LitElement {
.disabled=${!this._validJSON}
raised
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
"ui.panel.config.tools.tabs.states.set_state"
)}</ha-button
>
<ha-icon-button
@@ -290,7 +290,7 @@ class HaPanelDevState extends LitElement {
? html`<p>
<b
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_changed"
"ui.panel.config.tools.tabs.states.last_changed"
)}:</b
><br />
<a href=${this._historyFromLastChanged(this._entity)}
@@ -300,7 +300,7 @@ class HaPanelDevState extends LitElement {
<p>
<b
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_updated"
"ui.panel.config.tools.tabs.states.last_updated"
)}:</b
><br />
<a href=${this._historyFromLastUpdated(this._entity)}
@@ -312,7 +312,7 @@ class HaPanelDevState extends LitElement {
</div>
</div>
</ha-expansion-panel>
<developer-tools-state-renderer
<tools-state-renderer
.narrow=${this.narrow}
.entities=${entities}
.virtualize=${entities.length > VIRTUALIZE_THRESHOLD}
@@ -324,7 +324,7 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-entities"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_entities"
"ui.panel.config.tools.tabs.states.filter_entities"
)}
.value=${this._entityFilter}
@input=${this._entityFilterChanged}
@@ -332,7 +332,7 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-states"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_states"
"ui.panel.config.tools.tabs.states.filter_states"
)}
type="search"
.value=${this._stateFilter}
@@ -357,13 +357,13 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-attributes"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_attributes"
"ui.panel.config.tools.tabs.states.filter_attributes"
)}
type="search"
.value=${this._attributeFilter}
@input=${this._attributeFilterChanged}
></ha-input-search>
</developer-tools-state-renderer>
</tools-state-renderer>
`;
}
@@ -469,7 +469,7 @@ class HaPanelDevState extends LitElement {
if (!this._entityId) {
showAlertDialog(this, {
text: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.alert_entity_field"
"ui.panel.config.tools.tabs.states.alert_entity_field"
),
});
return;
@@ -758,6 +758,6 @@ class HaPanelDevState extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-state": HaPanelDevState;
"tools-state": HaPanelDevState;
}
}
@@ -132,7 +132,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
@click=${this._fetchOutliers}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.outliers"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.outliers"
)}
</ha-button>
<ha-button slot="primaryAction" data-dialog="close">
@@ -156,7 +156,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
@click=${this._fixIssue}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.adjust"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.adjust"
)}</ha-button
>
`;
@@ -166,7 +166,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.title"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.title"
)}
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
@@ -194,7 +194,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
} else if (this._statsHour.length < 1 && this._stats5min.length < 1) {
stats = html`<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found"
)}
</p>`;
} else {
@@ -234,20 +234,20 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
return html`
<div class="text-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.info_text_1"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.info_text_1"
)}
</div>
<div class="text-content">
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
)}</b
>
${this._params!.statistic.statistic_id}
</div>
<ha-selector-datetime
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.pick_a_time"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.pick_a_time"
)}
.hass=${this.hass}
.selector=${this._dateTimeSelector}
@@ -289,7 +289,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="text-content">
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
)}</b
>
${this._params!.statistic.statistic_id}
@@ -298,7 +298,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="table-row">
<span
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.start"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.start"
)}</span
>
<span
@@ -313,7 +313,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="table-row">
<span
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.end"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.end"
)}</span
>
<span
@@ -327,7 +327,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<ha-selector-number
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.new_value"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.new_value"
)}
.hass=${this.hass}
.selector=${this._amountSelector(unit || undefined, this._precision)}
@@ -506,7 +506,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
this._busy = false;
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted",
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted",
{ message: err.message || err }
),
});
@@ -514,7 +514,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted"
),
});
this._markDirtyStateClean();
@@ -53,13 +53,13 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.title"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.title"
)}
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_1",
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_1",
{
name: getStatisticLabel(
this.hass,
@@ -72,16 +72,16 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
}
)}<br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_2"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_2"
)}<br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_3"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_3"
)}
</p>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.how_to_fix"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.how_to_fix"
)}
.value=${this._action}
name="action"
@@ -89,13 +89,13 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
>
<ha-radio-option value="update" autofocus>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.update",
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.update",
this._params.issue.data
)}
</ha-radio-option>
<ha-radio-option value="clear">
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.clear`
`ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.clear`
)}
</ha-radio-option>
</ha-radio-group>
@@ -110,7 +110,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
</ha-button>
<ha-button slot="primaryAction" @click=${this._fixIssue}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
"ui.panel.config.tools.tabs.statistics.fix_issue.fix"
)}
</ha-button>
</ha-dialog-footer>
@@ -50,13 +50,13 @@ export class DialogStatisticsFix extends LitElement {
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.title`
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.title`
)}
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
{
name: getStatisticLabel(
this.hass,
@@ -67,24 +67,24 @@ export class DialogStatisticsFix extends LitElement {
...(issue.type === "mean_type_changed"
? {
metadata_mean_type: this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}`
`ui.panel.config.tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}`
),
state_mean_type: this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.state_mean_type}`
`ui.panel.config.tools.tabs.statistics.mean_type.${issue.data.state_mean_type}`
),
}
: {}),
}
)}<br /><br />
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
{ statistic_id: issue.data.statistic_id }
)}
${
issue.type === "mean_type_changed"
? html`<br /><br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3",
"ui.panel.config.tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3",
{ statistic_id: issue.data.statistic_id }
)}`
: issue.type === "entity_not_recorded"
@@ -98,7 +98,7 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`
: issue.type === "entity_no_longer_recorded"
@@ -111,22 +111,22 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`
: issue.type === "state_class_removed"
? html`<ul>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
@@ -134,18 +134,18 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
)}</a
>
</li>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
)}
</li>
</ul>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`
: nothing
@@ -195,15 +195,15 @@ export class DialogStatisticsFix extends LitElement {
title:
err.code === "timeout"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_timeout_title"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_failed"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_failed"
),
text:
err.code === "timeout"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_timeout_text"
)
: err.message,
});
@@ -98,7 +98,7 @@ type DisplayedStatisticData = StatisticData & {
issues_string?: string;
};
@customElement("developer-tools-statistics")
@customElement("tools-statistics")
class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -188,7 +188,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
`ui.panel.config.tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
@@ -204,7 +204,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
): DataTableColumnContainer<DisplayedStatisticData> => ({
displayName: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.name"
"ui.panel.config.tools.tabs.statistics.data_table.name"
),
main: true,
sortable: true,
@@ -225,7 +225,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
area: getAreaTableColumn(localize),
statistic_id: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id"
"ui.panel.config.tools.tabs.statistics.data_table.statistic_id"
),
sortable: true,
filterable: true,
@@ -233,7 +233,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
statistics_unit_of_measurement: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistics_unit"
"ui.panel.config.tools.tabs.statistics.data_table.statistics_unit"
),
sortable: true,
filterable: true,
@@ -241,7 +241,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
source: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.source"
"ui.panel.config.tools.tabs.statistics.data_table.source"
),
sortable: true,
filterable: true,
@@ -249,7 +249,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
issues_string: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.issue"
"ui.panel.config.tools.tabs.statistics.data_table.issue"
),
sortable: true,
filterable: true,
@@ -259,14 +259,12 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
template: (statistic) =>
html`${
statistic.issues_string ??
localize("ui.panel.config.developer-tools.tabs.statistics.no_issue")
localize("ui.panel.config.tools.tabs.statistics.no_issue")
}`,
},
fix: {
title: "",
label: localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
),
label: localize("ui.panel.config.tools.tabs.statistics.fix_issue.fix"),
type: "icon",
template: (statistic) =>
html`${
@@ -281,8 +279,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statistic.issues.some((issue) =>
FIXABLE_ISSUES.includes(issue.type)
)
? "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
: "ui.panel.config.developer-tools.tabs.statistics.fix_issue.info"
? "ui.panel.config.tools.tabs.statistics.fix_issue.fix"
: "ui.panel.config.tools.tabs.statistics.fix_issue.info"
)}
</ha-button>`
: "—"
@@ -293,9 +291,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
actions: {
title: "",
label: localize(
"ui.panel.config.developer-tools.tabs.statistics.adjust_sum"
),
label: localize("ui.panel.config.tools.tabs.statistics.adjust_sum"),
type: "icon-button",
showNarrow: true,
template: (statistic) =>
@@ -303,7 +299,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
? html`
<ha-icon-button
.label=${localize(
"ui.panel.config.developer-tools.tabs.statistics.adjust_sum"
"ui.panel.config.tools.tabs.statistics.adjust_sum"
)}
.path=${mdiSlopeUphill}
.statistic=${statistic}
@@ -500,7 +496,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectAllIssues}>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues"
"ui.panel.config.tools.tabs.statistics.data_table.select_all_issues"
)}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectNone}>
@@ -529,7 +525,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</div>
<ha-assist-chip
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.delete_selected"
"ui.panel.config.tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@click=${this._clearSelected}
@@ -563,7 +559,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._registries.areas
)}
.noDataText=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.no_statistics"
"ui.panel.config.tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
.selectable=${this._selectMode}
@@ -770,10 +766,10 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
await showConfirmationDialog(this, {
title: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.title"
"ui.panel.config.tools.tabs.statistics.multi_delete.title"
),
text: html`${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text",
"ui.panel.config.tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this._i18n.localize("ui.common.delete"),
@@ -925,6 +921,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-statistics": HaPanelDevStatistics;
"tools-statistics": HaPanelDevStatistics;
}
}
@@ -40,7 +40,7 @@ For loop example getting entity values in the weather domain:
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`;
@customElement("developer-tools-template")
@customElement("tools-template")
class HaPanelDevTemplate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -108,7 +108,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="content">
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.about"
"ui.panel.config.tools.tabs.templates.about"
)}
outlined
.expanded=${this._descriptionExpanded}
@@ -117,7 +117,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="description">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.description"
"ui.panel.config.tools.tabs.templates.description"
)}
</p>
<ul>
@@ -127,7 +127,7 @@ class HaPanelDevTemplate extends LitElement {
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.jinja_documentation"
"ui.panel.config.tools.tabs.templates.jinja_documentation"
)}
</a>
</li>
@@ -141,7 +141,7 @@ class HaPanelDevTemplate extends LitElement {
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.template_extensions"
"ui.panel.config.tools.tabs.templates.template_extensions"
)}</a
>
</li>
@@ -159,7 +159,7 @@ class HaPanelDevTemplate extends LitElement {
<ha-card
class="edit-pane"
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.editor"
"ui.panel.config.tools.tabs.templates.editor"
)}
>
<div class="card-content">
@@ -177,7 +177,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="card-actions">
<ha-button appearance="plain" @click=${this._restoreDemo}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.reset"
"ui.panel.config.tools.tabs.templates.reset"
)}
</ha-button>
<ha-button appearance="plain" @click=${this._clear}>
@@ -186,7 +186,7 @@ class HaPanelDevTemplate extends LitElement {
</div>
<ha-tip>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.keyboard_tip",
"ui.panel.config.tools.tabs.templates.keyboard_tip",
{
autocomplete: html`<kbd>Ctrl</kbd>+<kbd>Space</kbd>`,
}
@@ -197,7 +197,7 @@ class HaPanelDevTemplate extends LitElement {
<ha-card
class="render-pane"
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.result"
"ui.panel.config.tools.tabs.templates.result"
)}
>
<div class="card-content ha-scrollbar">
@@ -231,7 +231,7 @@ ${
}</pre>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.result_type"
"ui.panel.config.tools.tabs.templates.result_type"
)}:
${resultType}
</p>
@@ -240,7 +240,7 @@ ${
? html`
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.time"
"ui.panel.config.tools.tabs.templates.time"
)}
</p>
`
@@ -253,7 +253,7 @@ ${
? html`
<p class="all_listeners">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.all_listeners"
"ui.panel.config.tools.tabs.templates.all_listeners"
)}
</p>
`
@@ -262,7 +262,7 @@ ${
? html`
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.listeners"
"ui.panel.config.tools.tabs.templates.listeners"
)}
</p>
<ul>
@@ -273,7 +273,7 @@ ${
<li>
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.domain"
"ui.panel.config.tools.tabs.templates.domain"
)}</b
>: ${domain}
</li>
@@ -286,7 +286,7 @@ ${
<li>
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.entity"
"ui.panel.config.tools.tabs.templates.entity"
)}</b
>: ${entity_id}
</li>
@@ -297,7 +297,7 @@ ${
: !this._templateResult.listeners.time
? html`<span class="all_listeners">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.no_listeners"
"ui.panel.config.tools.tabs.templates.no_listeners"
)}
</span>`
: nothing
@@ -596,7 +596,7 @@ ${
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.confirm_reset"
"ui.panel.config.tools.tabs.templates.confirm_reset"
),
warning: true,
}))
@@ -612,7 +612,7 @@ ${
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.confirm_clear"
"ui.panel.config.tools.tabs.templates.confirm_clear"
),
warning: true,
}))
@@ -632,6 +632,6 @@ ${
declare global {
interface HTMLElementTagNameMap {
"developer-tools-template": HaPanelDevTemplate;
"tools-template": HaPanelDevTemplate;
}
}
@@ -3,8 +3,8 @@ import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../types";
@customElement("developer-tools-router")
class DeveloperToolsRouter extends HassRouterPage {
@customElement("tools-router")
class ToolsRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@@ -22,37 +22,37 @@ class DeveloperToolsRouter extends HassRouterPage {
showLoading: true,
routes: {
event: {
tag: "developer-tools-event",
load: () => import("./event/developer-tools-event"),
tag: "tools-event",
load: () => import("./event/tools-event"),
},
service: "action",
action: {
tag: "developer-tools-action",
load: () => import("./action/developer-tools-action"),
tag: "tools-action",
load: () => import("./action/tools-action"),
},
state: {
tag: "developer-tools-state",
load: () => import("./state/developer-tools-state"),
tag: "tools-state",
load: () => import("./state/tools-state"),
},
template: {
tag: "developer-tools-template",
load: () => import("./template/developer-tools-template"),
tag: "tools-template",
load: () => import("./template/tools-template"),
},
statistics: {
tag: "developer-tools-statistics",
load: () => import("./statistics/developer-tools-statistics"),
tag: "tools-statistics",
load: () => import("./statistics/tools-statistics"),
},
yaml: {
tag: "developer-yaml-config",
load: () => import("./yaml_configuration/developer-yaml-config"),
tag: "tools-yaml-config",
load: () => import("./yaml_configuration/tools-yaml-config"),
},
assist: {
tag: "developer-tools-assist",
load: () => import("./assist/developer-tools-assist"),
tag: "tools-assist",
load: () => import("./assist/tools-assist"),
},
debug: {
tag: "developer-tools-debug",
load: () => import("./debug/developer-tools-debug"),
tag: "tools-debug",
load: () => import("./debug/tools-debug"),
},
},
};
@@ -77,6 +77,6 @@ class DeveloperToolsRouter extends HassRouterPage {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-router": DeveloperToolsRouter;
"tools-router": ToolsRouter;
}
}
@@ -16,7 +16,7 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, Route, TranslationDict } from "../../../../types";
type ReloadableDomain = Exclude<
keyof TranslationDict["ui"]["panel"]["config"]["developer-tools"]["tabs"]["yaml"]["section"]["reloading"],
keyof TranslationDict["ui"]["panel"]["config"]["tools"]["tabs"]["yaml"]["section"]["reloading"],
"heading" | "introduction" | "reload"
>;
@@ -25,8 +25,8 @@ interface TranslatedReloadableDomain {
name: string;
}
@customElement("developer-yaml-config")
export class DeveloperYamlConfig extends LitElement {
@customElement("tools-yaml-config")
export class ToolsYamlConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -61,10 +61,10 @@ export class DeveloperYamlConfig extends LitElement {
domain,
name:
this.hass.localize(
`ui.panel.config.developer-tools.tabs.yaml.section.reloading.${domain}`
`ui.panel.config.tools.tabs.yaml.section.reloading.${domain}`
) ||
this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.reload",
"ui.panel.config.tools.tabs.yaml.section.reloading.reload",
{ domain: domainToName(this.hass.localize, domain) }
),
}))
@@ -80,12 +80,12 @@ export class DeveloperYamlConfig extends LitElement {
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.heading"
"ui.panel.config.tools.tabs.yaml.section.validation.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.introduction"
"ui.panel.config.tools.tabs.yaml.section.validation.introduction"
)}
${
!this._validateResult
@@ -103,10 +103,10 @@ export class DeveloperYamlConfig extends LitElement {
${
this._validateResult.result === "valid"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.valid"
"ui.panel.config.tools.tabs.yaml.section.validation.valid"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.invalid"
"ui.panel.config.tools.tabs.yaml.section.validation.invalid"
)
}
</div>
@@ -116,7 +116,7 @@ export class DeveloperYamlConfig extends LitElement {
? html`<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.errors"
"ui.panel.config.tools.tabs.yaml.section.validation.errors"
)}
>
<!-- prettier-ignore -->
@@ -131,7 +131,7 @@ export class DeveloperYamlConfig extends LitElement {
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.warnings"
"ui.panel.config.tools.tabs.yaml.section.validation.warnings"
)}
>
<!-- prettier-ignore -->
@@ -148,7 +148,7 @@ export class DeveloperYamlConfig extends LitElement {
<div class="card-actions">
<ha-button appearance="plain" @click=${this._validateConfig}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.check_config"
"ui.panel.config.tools.tabs.yaml.section.validation.check_config"
)}
</ha-button>
<ha-button
@@ -158,7 +158,7 @@ export class DeveloperYamlConfig extends LitElement {
.disabled=${this._validateResult?.result === "invalid"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.server_management.restart"
"ui.panel.config.tools.tabs.yaml.section.server_management.restart"
)}
</ha-button>
</div>
@@ -166,18 +166,18 @@ export class DeveloperYamlConfig extends LitElement {
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.heading"
"ui.panel.config.tools.tabs.yaml.section.reloading.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.introduction"
"ui.panel.config.tools.tabs.yaml.section.reloading.introduction"
)}
</div>
<div class="card-actions">
<ha-call-service-button domain="homeassistant" service="reload_all"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.all"
"ui.panel.config.tools.tabs.yaml.section.reloading.all"
)}
</ha-call-service-button>
</div>
@@ -186,7 +186,7 @@ export class DeveloperYamlConfig extends LitElement {
domain="homeassistant"
service="reload_core_config"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.core"
"ui.panel.config.tools.tabs.yaml.section.reloading.core"
)}
</ha-call-service-button>
</div>
@@ -269,6 +269,6 @@ export class DeveloperYamlConfig extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-yaml-config": DeveloperYamlConfig;
"tools-yaml-config": ToolsYamlConfig;
}
}
+7 -1
View File
@@ -273,7 +273,13 @@ const computeLogbookValue = (
if (item.entity_id && item.state) {
return {
text: stateObj
? localizeStateMessage(hass, item.state, stateObj, domain!)
? localizeStateMessage(
hass,
item.state,
stateObj,
domain!,
item.attributes
)
: item.state,
type: "state",
};
@@ -2,6 +2,7 @@ import { mdiVolumeHigh, mdiVolumeOff } from "@mdi/js";
import { html, nothing } from "lit";
import type { TemplateResult } from "lit";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-control-button";
import "../../../../components/ha-svg-icon";
import { forwardHaptic } from "../../../../data/haptics";
@@ -12,7 +13,7 @@ import {
import type { HomeAssistant } from "../../../../types";
export const renderMuteButton = (
hass: HomeAssistant,
localize: LocalizeFunc,
stateObj: MediaPlayerEntity,
showMuteButton: boolean | undefined,
disabled: boolean,
@@ -28,7 +29,7 @@ export const renderMuteButton = (
return html`
<ha-control-button
class="mute"
.label=${hass.localize(
.label=${localize(
`ui.card.media_player.${isMuted ? "media_volume_unmute" : "media_volume_mute"}`
)}
.disabled=${disabled}
@@ -43,13 +44,13 @@ export const renderMuteButton = (
export const toggleMediaPlayerMute = (
ev: Event,
hass: HomeAssistant,
callService: HomeAssistant["callService"],
stateObj: MediaPlayerEntity,
el: HTMLElement
): void => {
ev.stopPropagation();
forwardHaptic(el, "light");
hass.callService("media_player", "volume_mute", {
callService("media_player", "volume_mute", {
entity_id: stateObj.entity_id,
is_volume_muted: !stateObj.attributes.is_volume_muted,
});
@@ -1,12 +1,19 @@
import { consume } from "@lit/context";
import { mdiShieldOff } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
@@ -21,8 +28,9 @@ import {
setProtectedAlarmControlPanelMode,
supportedAlarmModes,
} from "../../../data/alarm_control_panel";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes";
@@ -31,6 +39,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsAlarmModesCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "alarm_control_panel";
};
export const supportsAlarmModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -39,8 +52,7 @@ export const supportsAlarmModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "alarm_control_panel";
return supportsAlarmModesCardFeatureFromState(stateObj);
};
@customElement("hui-alarm-modes-card-feature")
@@ -48,10 +60,20 @@ class HuiAlarmModeCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: AlarmControlPanelEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: AlarmModesCardFeatureConfig;
@state() _currentMode?: AlarmMode;
@@ -74,25 +96,10 @@ class HuiAlarmModeCardFeature
this._config = config;
}
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
AlarmControlPanelEntity | undefined;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentMode = this._getCurrentMode(this._stateObj);
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._currentMode = this._getCurrentMode(this._stateObj);
}
}
@@ -126,7 +133,11 @@ class HuiAlarmModeCardFeature
private async _setMode(mode: AlarmMode) {
await setProtectedAlarmControlPanelMode(
this,
this.hass!,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this._stateObj!,
mode
);
@@ -135,10 +146,9 @@ class HuiAlarmModeCardFeature
protected render(): TemplateResult | typeof nothing {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsAlarmModesCardFeature(this.hass, this.context)
!supportsAlarmModesCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -152,7 +162,7 @@ class HuiAlarmModeCardFeature
this._config.modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.localize(`ui.card.alarm_control_panel.modes.${mode}`),
label: this._localize(`ui.card.alarm_control_panel.modes.${mode}`),
path: ALARM_MODES[mode].path,
}));
@@ -160,7 +170,7 @@ class HuiAlarmModeCardFeature
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.alarm_control_panel.disarm")}
.label=${this._localize("ui.card.alarm_control_panel.disarm")}
@click=${this._disarm}
>
<ha-svg-icon .path=${mdiShieldOff}></ha-svg-icon>
@@ -175,7 +185,7 @@ class HuiAlarmModeCardFeature
.value=${this._currentMode}
@value-changed=${this._valueChanged}
hide-option-label
.label=${this.hass.localize("ui.card.alarm_control_panel.modes_label")}
.label=${this._localize("ui.card.alarm_control_panel.modes_label")}
style=${styleMap({
"--control-select-color": color,
"--modes-count": options.length.toString(),
@@ -1,5 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, LitElement, nothing, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { isNumericFromAttributes } from "../../../common/number/format_number";
import type { HomeAssistant } from "../../../types";
@@ -9,6 +11,11 @@ import type {
BarGaugeCardFeatureConfig,
} from "./types";
const supportsBarGaugeCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "sensor" && isNumericFromAttributes(stateObj.attributes);
};
export const supportsBarGaugeCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -17,16 +24,17 @@ export const supportsBarGaugeCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "sensor" && isNumericFromAttributes(stateObj.attributes);
return supportsBarGaugeCardFeatureFromState(stateObj);
};
@customElement("hui-bar-gauge-card-feature")
class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public context!: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
@state() private _config?: BarGaugeCardFeatureConfig;
static getStubConfig(): BarGaugeCardFeatureConfig {
@@ -50,15 +58,13 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this.context.entity_id ||
!this.hass.states[this.context.entity_id] ||
!supportsBarGaugeCardFeature(this.hass, this.context)
!this._stateObj ||
!supportsBarGaugeCardFeatureFromState(this._stateObj)
) {
return nothing;
}
const stateObj = this.hass.states[this.context.entity_id];
const stateObj = this._stateObj;
const min = this._config.min ?? 0;
const max = this._config.max ?? 100;
const value = parseFloat(stateObj.state);
@@ -1,15 +1,22 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { apiContext, servicesContext } from "../../../data/context";
import {
hasRequiredScriptFields,
requiredScriptFieldsFilled,
hasRequiredScriptFieldsForServices,
requiredScriptFieldsFilledForServices,
} from "../../../data/script";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -17,6 +24,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsButtonCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return ["button", "input_button", "scene", "script"].includes(domain);
};
export const supportsButtonCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -25,27 +37,33 @@ export const supportsButtonCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return ["button", "input_button", "scene", "script"].includes(domain);
return supportsButtonCardFeatureFromState(stateObj);
};
@customElement("hui-button-card-feature")
class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: servicesContext, subscribe: true })
private _services!: HomeAssistant["services"];
@state() private _config?: ButtonCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
private _pressButton() {
if (!this.hass || !this._stateObj) return;
if (!this._stateObj) return;
const domain = computeDomain(this._stateObj.entity_id);
const service =
@@ -54,8 +72,12 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
if (domain === "script") {
const entityId = this._stateObj.entity_id;
if (
hasRequiredScriptFields(this.hass!, entityId) &&
!requiredScriptFieldsFilled(this.hass!, entityId, this._config?.data)
hasRequiredScriptFieldsForServices(this._services, entityId) &&
!requiredScriptFieldsFilledForServices(
this._services,
entityId,
this._config?.data
)
) {
showMoreInfoDialog(this, {
entityId: entityId,
@@ -74,7 +96,7 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
: {}),
};
this.hass.callService(domain, service, serviceData);
this._api.callService(domain, service, serviceData);
}
static getStubConfig(): ButtonCardFeatureConfig {
@@ -93,10 +115,9 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsButtonCardFeature(this.hass, this.context)
!supportsButtonCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -108,10 +129,7 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
class="press-button"
@click=${this._pressButton}
>
${
this._config.action_name ??
this.hass.localize("ui.card.button.press")
}
${this._config.action_name ?? this._localize("ui.card.button.press")}
</ha-control-button>
</ha-control-button-group>
`;
@@ -1,4 +1,5 @@
import { mdiFan } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,14 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsClimateFanModesCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.FAN_MODE)
);
};
export const supportsClimateFanModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,11 +29,7 @@ export const supportsClimateFanModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.FAN_MODE)
);
return supportsClimateFanModesCardFeatureFromState(stateObj);
};
@customElement("hui-climate-fan-modes-card-feature")
@@ -63,9 +68,8 @@ class HuiClimateFanModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsClimateFanModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsClimateFanModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,4 +1,5 @@
import { mdiThermostat } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { html } from "lit";
import { customElement } from "lit/decorators";
@@ -25,6 +26,11 @@ interface HvacModeOption extends HuiModeSelectOption {
iconPath: string;
}
const supportsClimateHvacModesCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "climate";
};
export const supportsClimateHvacModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -33,8 +39,7 @@ export const supportsClimateHvacModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "climate";
return supportsClimateHvacModesCardFeatureFromState(stateObj);
};
@customElement("hui-climate-hvac-modes-card-feature")
@@ -60,7 +65,7 @@ class HuiClimateHvacModesCardFeature
protected readonly _serviceAction = "set_hvac_mode";
protected get _label(): string {
return this.hass!.localize("ui.card.climate.mode");
return this._localize("ui.card.climate.mode");
}
protected readonly _showDropdownOptionIcons = false;
@@ -94,7 +99,7 @@ class HuiClimateHvacModesCardFeature
}
protected _getOptions(): HvacModeOption[] {
if (!this._stateObj || !this.hass) {
if (!this._stateObj) {
return [];
}
@@ -106,7 +111,7 @@ class HuiClimateHvacModesCardFeature
return filterModes(orderedHvacModes, this._config?.hvac_modes).map(
(mode) => ({
value: mode,
label: this.hass!.formatEntityState(this._stateObj!, mode),
label: this._formatters.formatEntityState(this._stateObj!, mode),
iconPath: climateHvacModeIcon(mode),
})
);
@@ -121,9 +126,8 @@ class HuiClimateHvacModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsClimateHvacModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsClimateHvacModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,4 +1,5 @@
import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,16 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsClimatePresetModesCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.PRESET_MODE)
);
};
export const supportsClimatePresetModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,11 +31,7 @@ export const supportsClimatePresetModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.PRESET_MODE)
);
return supportsClimatePresetModesCardFeatureFromState(stateObj);
};
@customElement("hui-climate-preset-modes-card-feature")
@@ -65,9 +72,8 @@ class HuiClimatePresetModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsClimatePresetModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsClimatePresetModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,4 +1,5 @@
import { mdiArrowOscillating } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,16 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsClimateSwingHorizontalModesCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.SWING_HORIZONTAL_MODE)
);
};
export const supportsClimateSwingHorizontalModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,11 +31,7 @@ export const supportsClimateSwingHorizontalModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.SWING_HORIZONTAL_MODE)
);
return supportsClimateSwingHorizontalModesCardFeatureFromState(stateObj);
};
@customElement("hui-climate-swing-horizontal-modes-card-feature")
@@ -65,9 +72,8 @@ class HuiClimateSwingHorizontalModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsClimateSwingHorizontalModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,4 +1,5 @@
import { mdiArrowOscillating } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,16 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsClimateSwingModesCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.SWING_MODE)
);
};
export const supportsClimateSwingModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,11 +31,7 @@ export const supportsClimateSwingModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "climate" &&
supportsFeature(stateObj, ClimateEntityFeature.SWING_MODE)
);
return supportsClimateSwingModesCardFeatureFromState(stateObj);
};
@customElement("hui-climate-swing-modes-card-feature")
@@ -65,9 +72,8 @@ class HuiClimateSwingModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsClimateSwingModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsClimateSwingModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,14 +1,21 @@
import { consume } from "@lit/context";
import { mdiMinus, mdiPlus, mdiRestore } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import {
@@ -17,6 +24,11 @@ import {
type LovelaceCardFeatureContext,
} from "./types";
const supportsCounterActionsCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "counter";
};
export const supportsCounterActionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -25,8 +37,7 @@ export const supportsCounterActionsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "counter";
return supportsCounterActionsCardFeatureFromState(stateObj);
};
interface CounterButton {
@@ -65,18 +76,21 @@ class HuiCounterActionsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CounterActionsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: CounterActionsCardFeatureConfig;
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-counter-actions-card-feature-editor");
@@ -99,10 +113,9 @@ class HuiCounterActionsCardFeature
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsCounterActionsCardFeature(this.hass, this.context)
!supportsCounterActionsCardFeatureFromState(this._stateObj)
) {
return null;
}
@@ -118,7 +131,7 @@ class HuiCounterActionsCardFeature
return html`
<ha-control-button
.entry=${button}
.label=${this.hass!.localize(
.label=${this._localize(
// @ts-ignore
`ui.card.counter.actions.${button.translationKey}`
)}
@@ -138,7 +151,7 @@ class HuiCounterActionsCardFeature
private _onActionTap(ev): void {
ev.stopPropagation();
const entry = (ev.target! as any).entry as CounterButton;
this.hass!.callService("counter", entry.serviceName, {
this._api.callService("counter", entry.serviceName, {
entity_id: this._stateObj!.entity_id,
});
}
@@ -1,15 +1,23 @@
import { consume } from "@lit/context";
import { mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import {
computeCloseIcon,
computeOpenIcon,
} from "../../../common/entity/cover_icon";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { apiContext } from "../../../data/context";
import {
canClose,
canOpen,
@@ -17,7 +25,7 @@ import {
CoverEntityFeature,
type CoverEntity,
} from "../../../data/cover";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -25,6 +33,15 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsCoverOpenCloseCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE))
);
};
export const supportsCoverOpenCloseCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -33,12 +50,7 @@ export const supportsCoverOpenCloseCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE))
);
return supportsCoverOpenCloseCardFeatureFromState(stateObj);
};
@customElement("hui-cover-open-close-card-feature")
@@ -46,18 +58,21 @@ class HuiCoverOpenCloseCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CoverOpenCloseCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: CoverEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: CoverOpenCloseCardFeatureConfig;
static getStubConfig(): CoverOpenCloseCardFeatureConfig {
return {
@@ -74,21 +89,21 @@ class HuiCoverOpenCloseCardFeature
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "open_cover", {
this._api.callService("cover", "open_cover", {
entity_id: this._stateObj!.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "close_cover", {
this._api.callService("cover", "close_cover", {
entity_id: this._stateObj!.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "stop_cover", {
this._api.callService("cover", "stop_cover", {
entity_id: this._stateObj!.entity_id,
});
}
@@ -96,10 +111,9 @@ class HuiCoverOpenCloseCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsCoverOpenCloseCardFeature(this.hass, this.context)
!supportsCoverOpenCloseCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -110,7 +124,7 @@ class HuiCoverOpenCloseCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.OPEN)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.open_cover")}
.label=${this._localize("ui.card.cover.open_cover")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this._stateObj)}
>
@@ -125,7 +139,7 @@ class HuiCoverOpenCloseCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.STOP)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.stop_cover")}
.label=${this._localize("ui.card.cover.stop_cover")}
@click=${this._onStopTap}
.disabled=${!canStop(this._stateObj)}
>
@@ -138,7 +152,7 @@ class HuiCoverOpenCloseCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.CLOSE)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.close_cover")}
.label=${this._localize("ui.card.cover.close_cover")}
@click=${this._onCloseTap}
.disabled=${!canClose(this._stateObj)}
>
@@ -1,17 +1,35 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-slider";
import { coverSupportsPosition, type CoverEntity } from "../../../data/cover";
import {
apiContext,
entitiesContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -19,6 +37,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsCoverPositionCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "cover" && coverSupportsPosition(stateObj);
};
export const supportsCoverPositionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -27,8 +50,7 @@ export const supportsCoverPositionCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "cover" && coverSupportsPosition(stateObj);
return supportsCoverPositionCardFeatureFromState(stateObj);
};
@customElement("hui-cover-position-card-feature")
@@ -36,20 +58,34 @@ class HuiCoverPositionCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state() private _config?: CoverPositionCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: CoverEntity;
private get _stateObj(): CoverEntity | undefined {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!];
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities!: HomeAssistant["entities"];
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: CoverPositionCardFeatureConfig;
static getStubConfig(): CoverPositionCardFeatureConfig {
return {
@@ -67,10 +103,9 @@ class HuiCoverPositionCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsCoverPositionCardFeature(this.hass, this.context)
!supportsCoverPositionCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -104,14 +139,14 @@ class HuiCoverPositionCardFeature
show-handle
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this._localize,
this._stateObj,
this.hass.entities,
this._entities,
"current_position"
)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -120,7 +155,7 @@ class HuiCoverPositionCardFeature
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("cover", "set_cover_position", {
this._api.callService("cover", "set_cover_position", {
entity_id: this._stateObj!.entity_id,
position: value,
});
@@ -1,11 +1,19 @@
import { consume } from "@lit/context";
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { apiContext } from "../../../data/context";
import {
CoverEntityFeature,
canCloseTilt,
@@ -13,7 +21,7 @@ import {
canStopTilt,
type CoverEntity,
} from "../../../data/cover";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -21,6 +29,15 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsCoverTiltCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT))
);
};
export const supportsCoverTiltCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,12 +46,7 @@ export const supportsCoverTiltCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT))
);
return supportsCoverTiltCardFeatureFromState(stateObj);
};
@customElement("hui-cover-tilt-card-feature")
@@ -42,18 +54,21 @@ class HuiCoverTiltCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CoverTiltCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: CoverEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: CoverTiltCardFeatureConfig;
static getStubConfig(): CoverTiltCardFeatureConfig {
return {
@@ -70,21 +85,21 @@ class HuiCoverTiltCardFeature
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "open_cover_tilt", {
this._api.callService("cover", "open_cover_tilt", {
entity_id: this._stateObj!.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "close_cover_tilt", {
this._api.callService("cover", "close_cover_tilt", {
entity_id: this._stateObj!.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("cover", "stop_cover_tilt", {
this._api.callService("cover", "stop_cover_tilt", {
entity_id: this._stateObj!.entity_id,
});
}
@@ -92,10 +107,9 @@ class HuiCoverTiltCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsCoverTiltCardFeature(this.hass, this.context)
!supportsCoverTiltCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -106,7 +120,7 @@ class HuiCoverTiltCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.OPEN_TILT)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.open_tilt_cover")}
.label=${this._localize("ui.card.cover.open_tilt_cover")}
@click=${this._onOpenTap}
.disabled=${!canOpenTilt(this._stateObj)}
>
@@ -119,7 +133,7 @@ class HuiCoverTiltCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.STOP_TILT)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.stop_cover")}
.label=${this._localize("ui.card.cover.stop_cover")}
@click=${this._onStopTap}
.disabled=${!canStopTilt(this._stateObj)}
>
@@ -132,7 +146,7 @@ class HuiCoverTiltCardFeature
supportsFeature(this._stateObj, CoverEntityFeature.CLOSE_TILT)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.cover.close_tilt_cover")}
.label=${this._localize("ui.card.cover.close_tilt_cover")}
@click=${this._onCloseTap}
.disabled=${!canCloseTilt(this._stateObj)}
>
@@ -1,17 +1,35 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { CoverEntity } from "../../../data/cover";
import { coverSupportsTiltPosition } from "../../../data/cover";
import {
apiContext,
entitiesContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
import type { FrontendLocaleData } from "../../../data/translation";
import { generateTiltSliderTrackBackgroundGradient } from "../../../state-control/cover/ha-state-control-cover-tilt-position";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -21,6 +39,15 @@ import type {
const GRADIENT = generateTiltSliderTrackBackgroundGradient();
const supportsCoverTiltPositionCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" && coverSupportsTiltPosition(stateObj as CoverEntity)
);
};
export const supportsCoverTiltPositionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,10 +56,7 @@ export const supportsCoverTiltPositionCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" && coverSupportsTiltPosition(stateObj as CoverEntity)
);
return supportsCoverTiltPositionCardFeatureFromState(stateObj);
};
@customElement("hui-cover-tilt-position-card-feature")
@@ -40,20 +64,34 @@ class HuiCoverTiltPositionCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state() private _config?: CoverTiltPositionCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: CoverEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities!: HomeAssistant["entities"];
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: CoverTiltPositionCardFeatureConfig;
static getStubConfig(): CoverTiltPositionCardFeatureConfig {
return {
@@ -71,10 +109,9 @@ class HuiCoverTiltPositionCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsCoverTiltPositionCardFeature(this.hass, this.context)
!supportsCoverTiltPositionCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -105,14 +142,14 @@ class HuiCoverTiltPositionCardFeature
inverted
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this._localize,
this._stateObj,
this.hass.entities,
this._entities,
"current_tilt_position"
)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position}
.locale=${this.hass.locale}
.locale=${this._locale}
>
<div slot="background" class="gradient"></div
></ha-control-slider>
@@ -123,7 +160,7 @@ class HuiCoverTiltPositionCardFeature
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("cover", "set_cover_tilt_position", {
this._api.callService("cover", "set_cover_tilt_position", {
entity_id: this._stateObj!.entity_id,
tilt_position: value,
});
@@ -1,15 +1,29 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-slider";
import type { HomeAssistant } from "../../../types";
import { apiContext, internationalizationContext } from "../../../data/context";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +34,14 @@ import type {
const loadDatePickerDialog = () =>
import("../../../components/date-picker/ha-dialog-date-picker");
const supportsDateSetCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
(domain === "input_datetime" && stateObj.attributes.has_date) ||
["datetime", "date"].includes(domain)
);
};
export const supportsDateSetCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,32 +50,38 @@ export const supportsDateSetCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
(domain === "input_datetime" && stateObj.attributes.has_date) ||
["datetime", "date"].includes(domain)
);
return supportsDateSetCardFeatureFromState(stateObj);
};
@customElement("hui-date-set-card-feature")
class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: DateSetCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] ?? undefined;
}
private _pressButton(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
if (!this.hass || !this._stateObj) return;
if (!this._stateObj || !this._locale) return;
fireEvent(this, "show-dialog", {
dialogTag: "ha-dialog-date-picker",
@@ -63,14 +91,14 @@ class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
min: "1970-01-01",
value: this._stateObj.state,
onChange: (value) => this._dateChanged(value),
locale: this.hass.locale.language,
firstWeekday: firstWeekdayIndex(this.hass.locale),
locale: this._locale.language,
firstWeekday: firstWeekdayIndex(this._locale),
},
});
}
private _dateChanged(value: string | undefined) {
if (!this.hass || !this._stateObj || !value) return;
if (!this._stateObj || !value) return;
const domain = computeDomain(this._stateObj.entity_id);
const service = domain === "input_datetime" ? "set_datetime" : "set_value";
@@ -85,12 +113,12 @@ class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
selectedDate.getDate()
);
this.hass.callService(domain, service, {
this._api.callService(domain, service, {
entity_id: this._stateObj.entity_id,
datetime: dateObj.toISOString(),
});
} else {
this.hass.callService(domain, service, {
this._api.callService(domain, service, {
entity_id: this._stateObj.entity_id,
date: value,
});
@@ -113,10 +141,10 @@ class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsDateSetCardFeature(this.hass, this.context)
!this._locale ||
!supportsDateSetCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -128,7 +156,7 @@ class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
class="press-button"
@click=${this._pressButton}
>
${this.hass.localize("ui.card.date.set_date")}
${this._localize("ui.card.date.set_date")}
</ha-control-button>
</ha-control-button-group>
`;
@@ -1,3 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -16,6 +17,13 @@ import type {
const FAN_DIRECTIONS: FanDirection[] = ["forward", "reverse"];
const supportsFanDirectionCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.DIRECTION)
);
};
export const supportsFanDirectionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -24,10 +32,7 @@ export const supportsFanDirectionCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.DIRECTION)
);
return supportsFanDirectionCardFeatureFromState(stateObj);
};
@customElement("hui-fan-direction-card-feature")
@@ -52,21 +57,19 @@ class HuiFanDirectionCardFeature
}
protected _getOptions(): HuiModeSelectOption[] {
if (!this.hass) {
if (!this._stateObj) {
return [];
}
return FAN_DIRECTIONS.map((direction) => ({
value: direction,
label: this.hass!.localize(`ui.card.fan.${direction}`),
label: this._localize(`ui.card.fan.${direction}`),
}));
}
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsFanDirectionCardFeature(this.hass, this.context)
this._stateObj && supportsFanDirectionCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,18 +1,26 @@
import { consume } from "@lit/context";
import { mdiArrowOscillating, mdiArrowOscillatingOff } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { FanEntity } from "../../../data/fan";
import { FanEntityFeature } from "../../../data/fan";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +28,13 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsFanOscillateCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.OSCILLATE)
);
};
export const supportsFanOscilatteCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,10 +43,7 @@ export const supportsFanOscilatteCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.OSCILLATE)
);
return supportsFanOscillateCardFeatureFromState(stateObj);
};
@customElement("hui-fan-oscillate-card-feature")
@@ -39,21 +51,24 @@ class HuiFanOscillateCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: FanEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: FanOscillateCardFeatureConfig;
@state() _oscillate?: boolean;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
}
static getStubConfig(): FanOscillateCardFeatureConfig {
return {
type: "fan-oscillate",
@@ -67,16 +82,9 @@ class HuiFanOscillateCardFeature
this._config = config;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._oscillate = this._stateObj.attributes.oscillating;
}
protected willUpdate(changedProp: PropertyValues): void {
if (changedProp.has("_stateObj") && this._stateObj) {
this._oscillate = this._stateObj.attributes.oscillating;
}
}
@@ -98,7 +106,7 @@ class HuiFanOscillateCardFeature
}
private async _updateOscillate(oscillate: boolean) {
await this.hass!.callService("fan", "oscillate", {
await this._api.callService("fan", "oscillate", {
entity_id: this._stateObj!.entity_id,
oscillating: oscillate,
});
@@ -107,10 +115,9 @@ class HuiFanOscillateCardFeature
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsFanOscilatteCardFeature(this.hass, this.context)
!supportsFanOscillateCardFeatureFromState(this._stateObj)
) {
return null;
}
@@ -120,7 +127,7 @@ class HuiFanOscillateCardFeature
const yesNo = ["no", "yes"] as const;
const options = yesNo.map<ControlSelectOption>((oscillating) => ({
value: oscillating,
label: this.hass!.localize(`ui.common.${oscillating}`),
label: this._localize(`ui.common.${oscillating}`),
path:
oscillating === "yes" ? mdiArrowOscillating : mdiArrowOscillatingOff,
}));
@@ -131,7 +138,7 @@ class HuiFanOscillateCardFeature
.value=${this._oscillate ? "yes" : "no"}
@value-changed=${this._valueChanged}
hide-option-label
.label=${this.hass.localize("ui.card.fan.oscillate")}
.label=${this._localize("ui.card.fan.oscillate")}
style=${styleMap({
"--control-select-color": color,
})}
@@ -1,4 +1,5 @@
import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,13 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsFanPresetModesCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE)
);
};
export const supportsFanPresetModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,10 +28,7 @@ export const supportsFanPresetModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE)
);
return supportsFanPresetModesCardFeatureFromState(stateObj);
};
@customElement("hui-fan-preset-modes-card-feature")
@@ -62,9 +67,8 @@ class HuiFanPresetModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsFanPresetModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsFanPresetModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,13 +1,27 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-slider";
import {
apiContext,
entitiesContext,
formattersContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
import type { FanEntity, FanSpeed } from "../../../data/fan";
@@ -20,7 +34,13 @@ import {
fanPercentageToSpeed,
fanSpeedToPercentage,
} from "../../../data/fan";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -28,6 +48,13 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsFanSpeedCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED)
);
};
export const supportsFanSpeedCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -36,26 +63,41 @@ export const supportsFanSpeedCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED)
);
return supportsFanSpeedCardFeatureFromState(stateObj);
};
@customElement("hui-fan-speed-card-feature")
class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: FanSpeedCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: FanEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities!: HomeAssistant["entities"];
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: FanSpeedCardFeatureConfig;
static getStubConfig(): FanSpeedCardFeatureConfig {
return {
@@ -72,18 +114,17 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
private _localizeSpeed(speed: FanSpeed) {
if (speed === "on" || speed === "off") {
return this.hass!.formatEntityState(this._stateObj!, speed);
return this._formatters.formatEntityState(this._stateObj!, speed);
}
return this.hass!.localize(`ui.card.fan.speed.${speed}`) || speed;
return this._localize(`ui.card.fan.speed.${speed}`) || speed;
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsFanSpeedCardFeature(this.hass, this.context)
!supportsFanSpeedCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -112,9 +153,9 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
@value-changed=${this._speedValueChanged}
hide-option-label
.label=${computeAttributeNameDisplay(
this.hass.localize,
this._localize,
this._stateObj,
this.hass.entities,
this._entities,
"percentage"
)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
@@ -133,14 +174,14 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
.step=${this._stateObj.attributes.percentage_step ?? 1}
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this._localize,
this._stateObj,
this.hass.entities,
this._entities,
"percentage"
)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -150,7 +191,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
const percentage = fanSpeedToPercentage(this._stateObj!, speed);
this.hass!.callService("fan", "set_percentage", {
this._api.callService("fan", "set_percentage", {
entity_id: this._stateObj!.entity_id,
percentage: percentage,
});
@@ -160,7 +201,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("fan", "set_percentage", {
this._api.callService("fan", "set_percentage", {
entity_id: this._stateObj!.entity_id,
percentage: value,
});
@@ -1,4 +1,5 @@
import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -12,6 +13,14 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsHumidifierModesCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "humidifier" &&
supportsFeature(stateObj, HumidifierEntityFeature.MODES)
);
};
export const supportsHumidifierModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -20,11 +29,7 @@ export const supportsHumidifierModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "humidifier" &&
supportsFeature(stateObj, HumidifierEntityFeature.MODES)
);
return supportsHumidifierModesCardFeatureFromState(stateObj);
};
@customElement("hui-humidifier-modes-card-feature")
@@ -63,9 +68,8 @@ class HuiHumidifierModesCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsHumidifierModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsHumidifierModesCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,19 +1,31 @@
import { consume } from "@lit/context";
import { mdiPower, mdiWaterPercent } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import { apiContext, formattersContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
HumidifierEntity,
HumidifierState,
} from "../../../data/humidifier";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantFormatters,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -21,6 +33,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsHumidifierToggleCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier";
};
export const supportsHumidifierToggleCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,8 +46,7 @@ export const supportsHumidifierToggleCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier";
return supportsHumidifierToggleCardFeatureFromState(stateObj);
};
@customElement("hui-humidifier-toggle-card-feature")
@@ -38,22 +54,28 @@ class HuiHumidifierToggleCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HumidifierEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state() private _config?: HumidifierToggleCardFeatureConfig;
@state() _currentState?: HumidifierState;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
HumidifierEntity | undefined;
}
static getStubConfig(): HumidifierToggleCardFeatureConfig {
return {
type: "humidifier-toggle",
@@ -67,17 +89,10 @@ class HuiHumidifierToggleCardFeature
this._config = config;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentState = this._stateObj.state as HumidifierState;
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._currentState = this._stateObj.state as HumidifierState;
}
}
@@ -99,7 +114,7 @@ class HuiHumidifierToggleCardFeature
}
private async _setState(newState: HumidifierState) {
await this.hass!.callService(
await this._api.callService(
"humidifier",
newState === "on" ? "turn_on" : "turn_off",
{
@@ -111,10 +126,9 @@ class HuiHumidifierToggleCardFeature
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsHumidifierToggleCardFeature(this.hass, this.context)
!supportsHumidifierToggleCardFeatureFromState(this._stateObj)
) {
return null;
}
@@ -123,7 +137,7 @@ class HuiHumidifierToggleCardFeature
const options = ["off", "on"].map<ControlSelectOption>((entityState) => ({
value: entityState,
label: this.hass!.formatEntityState(this._stateObj!, entityState),
label: this._formatters.formatEntityState(this._stateObj!, entityState),
path: entityState === "on" ? mdiWaterPercent : mdiPower,
}));
@@ -133,7 +147,7 @@ class HuiHumidifierToggleCardFeature
.value=${this._currentState}
@value-changed=${this._valueChanged}
hide-option-label
.label=${this.hass.localize("ui.card.humidifier.state")}
.label=${this._localize("ui.card.humidifier.state")}
style=${styleMap({
"--control-select-color": color,
})}
@@ -1,16 +1,23 @@
import { consume } from "@lit/context";
import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { LawnMowerEntity } from "../../../data/lawn_mower";
import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -75,6 +82,14 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record<
}),
};
const supportsLawnMowerCommandCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "lawn_mower" &&
LAWN_MOWER_COMMANDS.some((c) => supportsLawnMowerCommand(stateObj, c))
);
};
export const supportsLawnMowerCommandCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -83,11 +98,7 @@ export const supportsLawnMowerCommandCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "lawn_mower" &&
LAWN_MOWER_COMMANDS.some((c) => supportsLawnMowerCommand(stateObj, c))
);
return supportsLawnMowerCommandCardFeatureFromState(stateObj);
};
@customElement("hui-lawn-mower-commands-card-feature")
@@ -95,19 +106,21 @@ class HuiLawnMowerCommandCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LawnMowerCommandsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LawnMowerEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
LawnMowerEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: LawnMowerCommandsCardFeatureConfig;
static getStubConfig(): LawnMowerCommandsCardFeatureConfig {
return {
@@ -132,7 +145,7 @@ class HuiLawnMowerCommandCardFeature
private _onCommandTap(ev): void {
ev.stopPropagation();
const entry = (ev.target! as any).entry as LawnMowerButton;
this.hass!.callService("lawn_mower", entry.serviceName, {
this._api.callService("lawn_mower", entry.serviceName, {
entity_id: this._stateObj!.entity_id,
});
}
@@ -140,10 +153,9 @@ class HuiLawnMowerCommandCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLawnMowerCommandCardFeature(this.hass, this.context)
!supportsLawnMowerCommandCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -161,7 +173,7 @@ class HuiLawnMowerCommandCardFeature
return html`
<ha-control-button
.entry=${button}
.label=${this.hass!.localize(
.label=${this._localize(
// @ts-ignore
`ui.dialogs.more_info_control.lawn_mower.${button.translationKey}`
)}
@@ -1,11 +1,25 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-slider";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { lightSupportsBrightness, type LightEntity } from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -13,6 +27,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsLightBrightnessCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsBrightness(stateObj);
};
export const supportsLightBrightnessCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -21,8 +40,7 @@ export const supportsLightBrightnessCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsBrightness(stateObj);
return supportsLightBrightnessCardFeatureFromState(stateObj);
};
@customElement("hui-light-brightness-card-feature")
@@ -30,18 +48,28 @@ class HuiLightBrightnessCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LightBrightnessCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LightEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as LightEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: LightBrightnessCardFeatureConfig;
static getStubConfig(): LightBrightnessCardFeatureConfig {
return {
@@ -59,10 +87,9 @@ class HuiLightBrightnessCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLightBrightnessCardFeature(this.hass, this.context)
!supportsLightBrightnessCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -83,9 +110,9 @@ class HuiLightBrightnessCardFeature
.showHandle=${stateActive(this._stateObj)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${this.hass.localize("ui.card.light.brightness")}
.label=${this._localize("ui.card.light.brightness")}
unit="%"
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -94,7 +121,7 @@ class HuiLightBrightnessCardFeature
ev.stopPropagation();
const value = ev.detail.value;
this.hass!.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this._stateObj!.entity_id,
brightness_pct: value,
});
@@ -1,9 +1,21 @@
import { consume } from "@lit/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type {
Connection,
UnsubscribeFunc,
HassEntity,
} from "home-assistant-js-websocket";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { apiContext, connectionContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
computeDefaultFavoriteColors,
@@ -11,7 +23,11 @@ import {
type LightColor,
lightSupportsFavoriteColors,
} from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConnection,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -29,6 +45,13 @@ import { getMoreInfoHintCardFeatureEditor } from "./get-more-info-hint-card-feat
const PILL_GAP = 8;
const PILL_MIN_SIZE = 32;
const supportsLightColorFavoritesCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsFavoriteColors(stateObj);
};
export const supportsLightColorFavoritesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -37,8 +60,7 @@ export const supportsLightColorFavoritesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsFavoriteColors(stateObj);
return supportsLightColorFavoritesCardFeatureFromState(stateObj);
};
@customElement("hui-light-color-favorites-card-feature")
@@ -46,10 +68,27 @@ class HuiLightColorFavoritesCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LightEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<HomeAssistantConnection, Connection>({
transformer: ({ connection }) => connection,
})
private _connection?: Connection;
@state() private _config?: LightColorFavoritesCardFeatureConfig;
@state() private _entry?: EntityRegistryEntry | null;
@@ -86,11 +125,11 @@ class HuiLightColorFavoritesCardFeature
}
private _subscribeEntityEntry() {
if (this.hass && this.context?.entity_id) {
if (this._connection && this.context?.entity_id) {
const id = this.context.entity_id;
try {
this._unsubEntityRegistry = subscribeEntityRegistry(
this.hass!.connection,
this._connection,
(entries) => {
const entry = entries.find((e) => e.entity_id === id);
if (entry) {
@@ -108,15 +147,8 @@ class HuiLightColorFavoritesCardFeature
return this._resizeController.value ?? 0;
}
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as LightEntity | undefined;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("context")) {
if (changedProps.has("context") || changedProps.has("_connection")) {
this._unsubscribeEntityRegistry();
this._subscribeEntityEntry();
}
@@ -150,10 +182,9 @@ class HuiLightColorFavoritesCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLightColorFavoritesCardFeature(this.hass, this.context)
!supportsLightColorFavoritesCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -165,7 +196,7 @@ class HuiLightColorFavoritesCardFeature
${visibleColors.map(
(color, index) => html`
<ha-favorite-color-button
.label=${this.hass!.localize(
.label=${this._localize(
`ui.dialogs.more_info_control.light.favorite_color.set`,
{ number: index }
)}
@@ -189,7 +220,7 @@ class HuiLightColorFavoritesCardFeature
const index = (ev.target! as any).index!;
const favorite = this._favoriteColors[index];
this.hass!.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this._stateObj!.entity_id,
...favorite,
});
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -6,9 +8,16 @@ import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
} from "../../../common/color/convert-light-color";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-slider";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
import {
@@ -16,8 +25,13 @@ import {
lightSupportsColorMode,
type LightEntity,
} from "../../../data/light";
import type { FrontendLocaleData } from "../../../data/translation";
import { generateColorTemperatureGradient } from "../../../dialogs/more-info/components/lights/light-color-temp-picker";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -25,6 +39,14 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsLightColorTempCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "light" &&
lightSupportsColorMode(stateObj, LightColorMode.COLOR_TEMP)
);
};
export const supportsLightColorTempCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -33,11 +55,7 @@ export const supportsLightColorTempCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "light" &&
lightSupportsColorMode(stateObj, LightColorMode.COLOR_TEMP)
);
return supportsLightColorTempCardFeatureFromState(stateObj);
};
@customElement("hui-light-color-temp-card-feature")
@@ -45,18 +63,28 @@ class HuiLightColorTempCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LightColorTempCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LightEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LightEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: LightColorTempCardFeatureConfig;
static getStubConfig(): LightColorTempCardFeatureConfig {
return {
@@ -74,10 +102,9 @@ class HuiLightColorTempCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLightColorTempCardFeature(this.hass, this.context)
!supportsLightColorTempCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -101,14 +128,14 @@ class HuiLightColorTempCardFeature
.showHandle=${stateActive(this._stateObj)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${this.hass.localize("ui.card.light.color_temperature")}
.label=${this._localize("ui.card.light.color_temperature")}
.min=${minKelvin}
.max=${maxKelvin}
style=${styleMap({
"--gradient": gradient,
})}
.unit=${DOMAIN_ATTRIBUTES_UNITS.light.color_temp_kelvin}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -121,7 +148,7 @@ class HuiLightColorTempCardFeature
ev.stopPropagation();
const value = ev.detail.value;
this.hass!.callService("light", "turn_on", {
this._api.callService("light", "turn_on", {
entity_id: this._stateObj!.entity_id,
color_temp_kelvin: value,
});
@@ -1,10 +1,18 @@
import { consume } from "@lit/context";
import { mdiLock, mdiLockOpenVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { apiContext } from "../../../data/context";
import { forwardHaptic } from "../../../data/haptics";
import {
callProtectedLockService,
@@ -12,7 +20,7 @@ import {
canUnlock,
type LockEntity,
} from "../../../data/lock";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +28,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsLockCommandsCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock";
};
export const supportsLockCommandsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,8 +41,7 @@ export const supportsLockCommandsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "lock";
return supportsLockCommandsCardFeatureFromState(stateObj);
};
@customElement("hui-lock-commands-card-feature")
@@ -37,18 +49,21 @@ class HuiLockCommandsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LockCommandsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LockEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LockEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: LockCommandsCardFeatureConfig;
static getStubConfig(): LockCommandsCardFeatureConfig {
return {
@@ -66,20 +81,28 @@ class HuiLockCommandsCardFeature
private _onTap(ev): void {
ev.stopPropagation();
const service = ev.target.dataset.service;
if (!this.hass || !this._stateObj || !service) {
if (!this._stateObj || !service) {
return;
}
forwardHaptic(this, "light");
callProtectedLockService(this, this.hass, this._stateObj, service);
callProtectedLockService(
this,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this._stateObj,
service
);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLockCommandsCardFeature(this.hass, this.context)
!supportsLockCommandsCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -87,7 +110,7 @@ class HuiLockCommandsCardFeature
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.lock")}
.label=${this._localize("ui.card.lock.lock")}
.disabled=${!canLock(this._stateObj)}
@click=${this._onTap}
data-service="lock"
@@ -95,7 +118,7 @@ class HuiLockCommandsCardFeature
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.unlock")}
.label=${this._localize("ui.card.lock.unlock")}
.disabled=${!canUnlock(this._stateObj)}
@click=${this._onTap}
data-service="unlock"
@@ -1,18 +1,26 @@
import { consume } from "@lit/context";
import { mdiCheck } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { apiContext } from "../../../data/context";
import {
callProtectedLockService,
canOpen,
LockEntityFeature,
type LockEntity,
} from "../../../data/lock";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +28,11 @@ import type {
LovelaceCardFeatureContext,
} from "./types";
const supportsLockOpenDoorCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
};
export const supportsLockOpenDoorCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,8 +41,7 @@ export const supportsLockOpenDoorCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
return supportsLockOpenDoorCardFeatureFromState(stateObj);
};
const CONFIRM_TIMEOUT_SECOND = 5;
@@ -42,23 +54,26 @@ class HuiLockOpenDoorCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: LockEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() public _buttonState: ButtonState = "normal";
@state() private _config?: LockOpenDoorCardFeatureConfig;
private _buttonTimeout?: number;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LockEntity | undefined;
}
static getStubConfig(): LockOpenDoorCardFeatureConfig {
return {
type: "lock-open-door",
@@ -87,10 +102,19 @@ class HuiLockOpenDoorCardFeature
this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND);
return;
}
if (!this.hass || !this._stateObj) {
if (!this._stateObj) {
return;
}
callProtectedLockService(this, this.hass, this._stateObj!, "open");
callProtectedLockService(
this,
{
callService: this._api.callService,
callWS: this._api.callWS,
localize: this._localize,
},
this._stateObj,
"open"
);
this._setButtonState("done", DONE_TIMEOUT_SECOND);
}
@@ -98,10 +122,9 @@ class HuiLockOpenDoorCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsLockOpenDoorCardFeature(this.hass, this.context)
!supportsLockOpenDoorCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -112,7 +135,7 @@ class HuiLockOpenDoorCardFeature
? html`
<p class="open-done">
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
${this.hass.localize("ui.card.lock.open_door_done")}
${this._localize("ui.card.lock.open_door_done")}
</p>
`
: html`
@@ -124,8 +147,8 @@ class HuiLockOpenDoorCardFeature
>
${
this._buttonState === "confirm"
? this.hass.localize("ui.card.lock.open_door_confirm")
: this.hass.localize("ui.card.lock.open_door")
? this._localize("ui.card.lock.open_door_confirm")
: this._localize("ui.card.lock.open_door")
}
</ha-control-button>
</ha-control-button-group>
@@ -1,14 +1,22 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { apiContext } from "../../../data/context";
import type {
ControlButton,
MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import { hasConfigChanged } from "../common/has-changed";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
@@ -21,6 +29,13 @@ import type {
MediaPlayerPlaybackCardFeatureConfig,
} from "./types";
const supportsMediaPlayerPlaybackCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "media_player";
};
export const supportsMediaPlayerPlaybackCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,8 +44,7 @@ export const supportsMediaPlayerPlaybackCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "media_player";
return supportsMediaPlayerPlaybackCardFeatureFromState(stateObj);
};
@customElement("hui-media-player-playback-card-feature")
@@ -38,24 +52,26 @@ class HuiMediaPlayerPlaybackCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: MediaPlayerEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: MediaPlayerPlaybackCardFeatureConfig;
@state() private _narrow?: boolean = false;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
MediaPlayerEntity | undefined;
}
static getStubConfig(): MediaPlayerPlaybackCardFeatureConfig {
return {
type: "media-player-playback",
@@ -82,25 +98,18 @@ class HuiMediaPlayerPlaybackCardFeature
}
}
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const entityId = this.context?.entity_id;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
hasConfigChanged(this, changedProps) ||
(changedProps.has("hass") &&
(!oldHass ||
!entityId ||
oldHass.states[entityId] !== this.hass!.states[entityId]))
hasConfigChanged(this, changedProps) || changedProps.has("_stateObj")
);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!supportsMediaPlayerPlaybackCardFeature(this.hass, this.context) ||
!this._stateObj
!this._stateObj ||
!supportsMediaPlayerPlaybackCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -113,9 +122,7 @@ class HuiMediaPlayerPlaybackCardFeature
(button) => html`
<ha-control-button
key=${button.action}
.label=${this.hass?.localize(
`ui.card.media_player.${button.action}`
)}
.label=${this._localize(`ui.card.media_player.${button.action}`)}
.disabled=${button.disabled}
@click=${this._action}
>
@@ -166,7 +173,7 @@ class HuiMediaPlayerPlaybackCardFeature
if (!action) return;
if (action === "volume_mute") {
this.hass!.callService("media_player", "volume_mute", {
this._api.callService("media_player", "volume_mute", {
entity_id: this._stateObj.entity_id,
is_volume_muted: !this._stateObj.attributes.is_volume_muted,
});
@@ -174,7 +181,7 @@ class HuiMediaPlayerPlaybackCardFeature
}
if (action === "shuffle") {
this.hass!.callService("media_player", "shuffle_set", {
this._api.callService("media_player", "shuffle_set", {
entity_id: this._stateObj.entity_id,
shuffle: !this._stateObj.attributes.shuffle,
});
@@ -183,14 +190,14 @@ class HuiMediaPlayerPlaybackCardFeature
if (action === "repeat") {
const repeat = this._stateObj.attributes.repeat ?? "off";
this.hass!.callService("media_player", "repeat_set", {
this._api.callService("media_player", "repeat_set", {
entity_id: this._stateObj.entity_id,
repeat: repeat === "off" ? "one" : repeat === "one" ? "all" : "off",
});
return;
}
this.hass!.callService("media_player", action, {
this._api.callService("media_player", action, {
entity_id: this._stateObj.entity_id,
});
}
@@ -1,4 +1,5 @@
import type { PropertyValues } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -15,6 +16,17 @@ import type {
MediaPlayerSoundModeCardFeatureConfig,
} from "./types";
const supportsMediaPlayerSoundModeCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) &&
!!stateObj.attributes.sound_mode_list?.length
);
};
export const supportsMediaPlayerSoundModeCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -23,12 +35,7 @@ export const supportsMediaPlayerSoundModeCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) &&
!!stateObj.attributes.sound_mode_list?.length
);
return supportsMediaPlayerSoundModeCardFeatureFromState(stateObj);
};
@customElement("hui-media-player-sound-mode-card-feature")
@@ -48,7 +55,7 @@ class HuiMediaPlayerSoundModeCardFeature
protected readonly _serviceAction = "select_sound_mode";
protected get _label(): string {
return this.hass!.localize("ui.card.media_player.sound_mode");
return this._localize("ui.card.media_player.sound_mode");
}
protected readonly _hideLabel = false;
@@ -76,25 +83,18 @@ class HuiMediaPlayerSoundModeCardFeature
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
const entityId = this.context?.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
return (
changedProps.has("_currentValue") ||
changedProps.has("context") ||
hasConfigChanged(this, changedProps) ||
(changedProps.has("hass") &&
(!oldHass ||
!entityId ||
oldHass.states[entityId] !== this.hass?.states[entityId]))
changedProps.has("_stateObj") ||
hasConfigChanged(this, changedProps)
);
}
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsMediaPlayerSoundModeCardFeature(this.hass, this.context)
this._stateObj &&
supportsMediaPlayerSoundModeCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,4 +1,5 @@
import type { PropertyValues } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -15,6 +16,16 @@ import type {
MediaPlayerSourceCardFeatureConfig,
} from "./types";
const supportsMediaPlayerSourceCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE)
);
};
export const supportsMediaPlayerSourceCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -23,11 +34,7 @@ export const supportsMediaPlayerSourceCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE)
);
return supportsMediaPlayerSourceCardFeatureFromState(stateObj);
};
@customElement("hui-media-player-source-card-feature")
@@ -52,7 +59,7 @@ class HuiMediaPlayerSourceCardFeature
protected readonly _serviceAction = "select_source";
protected get _label(): string {
return this.hass!.localize("ui.card.media_player.source");
return this._localize("ui.card.media_player.source");
}
protected readonly _hideLabel = false;
@@ -75,25 +82,18 @@ class HuiMediaPlayerSourceCardFeature
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
const entityId = this.context?.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
return (
changedProps.has("_currentValue") ||
changedProps.has("context") ||
hasConfigChanged(this, changedProps) ||
(changedProps.has("hass") &&
(!oldHass ||
!entityId ||
oldHass.states[entityId] !== this.hass?.states[entityId]))
changedProps.has("_stateObj") ||
hasConfigChanged(this, changedProps)
);
}
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsMediaPlayerSourceCardFeature(this.hass, this.context)
this._stateObj &&
supportsMediaPlayerSourceCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,15 +1,29 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { clamp } from "../../../common/number/clamp";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-number-buttons";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import {
@@ -21,6 +35,16 @@ import type {
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "./types";
const supportsMediaPlayerVolumeButtonsCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
export const supportsMediaPlayerVolumeButtonsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,11 +53,7 @@ export const supportsMediaPlayerVolumeButtonsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
return supportsMediaPlayerVolumeButtonsCardFeatureFromState(stateObj);
};
@customElement("hui-media-player-volume-buttons-card-feature")
@@ -41,19 +61,28 @@ class HuiMediaPlayerVolumeButtonsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: MediaPlayerEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
MediaPlayerEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
return {
@@ -79,10 +108,9 @@ class HuiMediaPlayerVolumeButtonsCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
!supportsMediaPlayerVolumeButtonsCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -98,7 +126,7 @@ class HuiMediaPlayerVolumeButtonsCardFeature
return html`
<ha-control-number-buttons
.disabled=${disabled}
.locale=${this.hass.locale}
.locale=${this._locale}
min="0"
max="100"
.step=${this._config.step ?? 5}
@@ -107,7 +135,7 @@ class HuiMediaPlayerVolumeButtonsCardFeature
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
${renderMuteButton(
this.hass,
this._localize,
stateObj,
this._config.show_mute_button,
disabled,
@@ -119,14 +147,14 @@ class HuiMediaPlayerVolumeButtonsCardFeature
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.hass!.callService("media_player", "volume_set", {
this._api.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: clamp(ev.detail.value, 0, 100) / 100,
});
}
private _toggleMute = (ev: Event) => {
toggleMediaPlayerMute(ev, this.hass!, this._stateObj!, this);
toggleMediaPlayerMute(ev, this._api!.callService, this._stateObj!, this);
};
static get styles() {
@@ -1,15 +1,29 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-slider";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import {
@@ -21,6 +35,16 @@ import type {
MediaPlayerVolumeSliderCardFeatureConfig,
} from "./types";
const supportsMediaPlayerVolumeSliderCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
export const supportsMediaPlayerVolumeSliderCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -29,11 +53,7 @@ export const supportsMediaPlayerVolumeSliderCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
return supportsMediaPlayerVolumeSliderCardFeatureFromState(stateObj);
};
@customElement("hui-media-player-volume-slider-card-feature")
@@ -41,19 +61,28 @@ class HuiMediaPlayerVolumeSliderCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: MediaPlayerEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
MediaPlayerEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig;
static getStubConfig(): MediaPlayerVolumeSliderCardFeatureConfig {
return {
@@ -78,10 +107,9 @@ class HuiMediaPlayerVolumeSliderCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeSliderCardFeature(this.hass, this.context)
!supportsMediaPlayerVolumeSliderCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -103,10 +131,10 @@ class HuiMediaPlayerVolumeSliderCardFeature
.disabled=${disabled}
@value-changed=${this._valueChanged}
unit="%"
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
${renderMuteButton(
this.hass,
this._localize,
stateObj,
this._config.show_mute_button,
disabled,
@@ -119,14 +147,14 @@ class HuiMediaPlayerVolumeSliderCardFeature
ev.stopPropagation();
const value = ev.detail.value;
this.hass!.callService("media_player", "volume_set", {
this._api.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: value / 100,
});
}
private _toggleMute = (ev: Event) => {
toggleMediaPlayerMute(ev, this.hass!, this._stateObj!, this);
toggleMediaPlayerMute(ev, this._api!.callService, this._stateObj!, this);
};
static get styles() {
@@ -1,14 +1,21 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-svg-icon";
import { apiContext, formattersContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistantApi, HomeAssistantFormatters } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes";
@@ -38,10 +45,24 @@ export abstract class HuiModeSelectCardFeatureBase<
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
protected _stateObj?: TEntity;
@state()
@consumeLocalize()
protected _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
protected _api!: HomeAssistantApi;
@state()
@consume({ context: formattersContext, subscribe: true })
protected _formatters!: HomeAssistantFormatters;
@state() protected _config?: TConfig;
@state() protected _currentValue?: string;
@@ -63,7 +84,7 @@ export abstract class HuiModeSelectCardFeatureBase<
protected abstract _isSupported(): boolean;
protected get _label(): string {
return this.hass!.formatEntityAttributeName(
return this._formatters.formatEntityAttributeName(
this._stateObj!,
this._attribute
);
@@ -90,14 +111,6 @@ export abstract class HuiModeSelectCardFeatureBase<
return true;
}
protected get _stateObj(): TEntity | undefined {
if (!this.hass || !this.context?.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as TEntity | undefined;
}
public setConfig(config: TConfig): void {
if (!config) {
throw new Error("Invalid configuration");
@@ -106,28 +119,17 @@ export abstract class HuiModeSelectCardFeatureBase<
this._config = config;
}
protected willUpdate(changedProps: PropertyValues<this>): void {
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (
(changedProps.has("hass") || changedProps.has("context")) &&
this._stateObj
) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldStateObj = this.context?.entity_id
? (oldHass?.states[this.context.entity_id] as TEntity | undefined)
: undefined;
if (oldStateObj !== this._stateObj) {
this._currentValue = this._getValue(this._stateObj);
}
if (changedProps.has("_stateObj") && this._stateObj) {
this._currentValue = this._getValue(this._stateObj);
}
}
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!this._isSupported()
@@ -191,7 +193,7 @@ export abstract class HuiModeSelectCardFeatureBase<
}
protected _getOptions(): HuiModeSelectOption[] {
if (!this._stateObj || !this.hass) {
if (!this._stateObj) {
return [];
}
@@ -200,7 +202,7 @@ export abstract class HuiModeSelectCardFeatureBase<
this._configuredModes
).map((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
this._stateObj!,
this._attribute,
mode
@@ -225,7 +227,7 @@ export abstract class HuiModeSelectCardFeatureBase<
></ha-attribute-icon>`;
private async _valueChanged(ev: AttributeModeChangeEvent) {
if (!this.hass || !this._stateObj) {
if (!this._stateObj) {
return;
}
@@ -243,7 +245,7 @@ export abstract class HuiModeSelectCardFeatureBase<
this._currentValue = value;
try {
await this.hass.callService(
await this._api.callService(
this._getServiceDomain(this._stateObj),
this._serviceAction,
{
@@ -1,22 +1,40 @@
import { consume } from "@lit/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type {
Connection,
HassEntity,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeKeys } from "../../../common/translations/localize";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import "../../../components/ha-control-select";
import { apiContext, connectionContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
getExtendedEntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConnection,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -50,6 +68,15 @@ export interface NumericFavoriteCardFeatureDefinition<
featureLabelKey: LocalizeKeys;
}
const supportsNumericFavoriteCardFeatureFromState = <
TEntity extends NumericFavoriteEntity,
>(
stateObj: TEntity,
definition: NumericFavoriteCardFeatureDefinition<TEntity>
) =>
computeDomain(stateObj.entity_id) === definition.domain &&
definition.supportsPosition(stateObj);
export const supportsNumericFavoriteCardFeature = <
TEntity extends NumericFavoriteEntity,
>(
@@ -65,10 +92,7 @@ export const supportsNumericFavoriteCardFeature = <
return false;
}
return (
computeDomain(stateObj.entity_id) === definition.domain &&
definition.supportsPosition(stateObj)
);
return supportsNumericFavoriteCardFeatureFromState(stateObj, definition);
};
export abstract class HuiNumericFavoriteCardFeatureBase<
@@ -78,12 +102,29 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
protected _stateObj?: TEntity;
@state()
@consumeLocalize()
protected _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
protected _api!: HomeAssistantApi;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<HomeAssistantConnection, Connection>({
transformer: ({ connection }) => connection,
})
protected _connection?: Connection;
@state() protected _config?: TConfig;
@state() protected _entry?: ExtEntityRegistryEntry | null;
@@ -108,14 +149,6 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
protected abstract get _definition(): NumericFavoriteCardFeatureDefinition<TEntity>;
protected get _stateObj(): TEntity | undefined {
if (!this.hass || !this.context?.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as TEntity | undefined;
}
public connectedCallback() {
super.connectedCallback();
this._refreshEntitySubscription();
@@ -134,38 +167,14 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
this._config = config as TConfig;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = this.context?.entity_id
? (oldHass?.states[this.context.entity_id] as TEntity | undefined)
: undefined;
if (oldStateObj !== this._stateObj) {
this._currentPosition = this._definition.getCurrentValue(
this._stateObj
);
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._currentPosition = this._definition.getCurrentValue(this._stateObj);
}
if (
changedProp.has("context") &&
(changedProp.get("context") as LovelaceCardFeatureContext | undefined)
?.entity_id !== this.context?.entity_id
) {
this._refreshEntitySubscription();
}
if (
changedProp.has("hass") &&
(changedProp.get("hass") as HomeAssistant | undefined)?.connection !==
this.hass?.connection
) {
if (changedProp.has("context") || changedProp.has("_connection")) {
this._refreshEntitySubscription();
}
}
@@ -182,12 +191,8 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
}
private async _loadEntityEntry(entityId: string): Promise<void> {
if (!this.hass) {
return;
}
try {
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
const entry = await getExtendedEntityRegistryEntry(this._api, entityId);
if (this.context?.entity_id === entityId) {
this._entry = entry;
@@ -206,7 +211,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
try {
this._unsubEntityRegistry = subscribeEntityRegistry(
this.hass!.connection,
this._connection!,
async (entries) => {
if (this.context?.entity_id !== entityId) {
return;
@@ -227,9 +232,9 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
private async _ensureEntitySubscription(): Promise<void> {
const entityId = this.context?.entity_id;
const connection = this.hass?.connection;
const connection = this._connection;
if (!this.hass || !entityId || !connection) {
if (!entityId || !connection) {
this._unsubscribeEntityRegistry();
this._subscribedEntityId = undefined;
this._subscribedConnection = undefined;
@@ -256,7 +261,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
) {
const value = ev.detail.value;
if (value == null || !this.hass || !this._stateObj) {
if (value == null || !this._stateObj) {
return;
}
@@ -275,7 +280,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
this._currentPosition = position;
try {
await this.hass.callService(
await this._api.callService(
this._definition.domain,
this._definition.setPositionService,
{
@@ -291,12 +296,10 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsNumericFavoriteCardFeature(
this.hass,
this.context,
!supportsNumericFavoriteCardFeatureFromState(
this._stateObj,
this._definition
)
) {
@@ -308,9 +311,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
this._definition.defaultFavoritePositions
);
const hass = this.hass;
if (positions.length === 0 || !hass) {
if (positions.length === 0) {
return null;
}
@@ -321,7 +322,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
const options = visiblePositions.map((position) => ({
value: String(position),
label: `${position}%`,
ariaLabel: hass.localize(this._definition.setPositionLabelKey, {
ariaLabel: this._localize(this._definition.setPositionLabelKey, {
value: `${position}%`,
}),
}));
@@ -339,7 +340,7 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
.options=${options}
.value=${currentValue}
@value-changed=${this._valueChanged}
.label=${hass.localize(this._definition.featureLabelKey)}
.label=${this._localize(this._definition.featureLabelKey)}
.disabled=${this._stateObj.state === UNAVAILABLE}
>
</ha-control-select>
@@ -1,15 +1,24 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-number-buttons";
import "../../../components/ha-control-slider";
import "../../../components/ha-icon";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -17,6 +26,11 @@ import type {
NumericInputCardFeatureConfig,
} from "./types";
const supportsNumericInputCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "input_number" || domain === "number";
};
export const supportsNumericInputCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -25,8 +39,7 @@ export const supportsNumericInputCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "input_number" || domain === "number";
return supportsNumericInputCardFeatureFromState(stateObj);
};
@customElement("hui-numeric-input-card-feature")
@@ -34,10 +47,23 @@ class HuiNumericInputCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: NumericInputCardFeatureConfig;
@state() _currentState?: string;
@@ -49,13 +75,6 @@ class HuiNumericInputCardFeature
};
}
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-numeric-input-card-feature-editor");
return document.createElement("hui-numeric-input-card-feature-editor");
@@ -68,17 +87,10 @@ class HuiNumericInputCardFeature
this._config = config;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentState = this._stateObj.state;
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._currentState = this._stateObj.state;
}
}
@@ -87,7 +99,7 @@ class HuiNumericInputCardFeature
const domain = computeDomain(stateObj.entity_id);
await this.hass!.callService(domain, "set_value", {
await this._api.callService(domain, "set_value", {
entity_id: stateObj.entity_id,
value: ev.detail.value,
});
@@ -96,10 +108,9 @@ class HuiNumericInputCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsNumericInputCardFeature(this.hass, this.context)
!supportsNumericInputCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -119,7 +130,7 @@ class HuiNumericInputCardFeature
@value-changed=${this._setValue}
.disabled=${stateObj.state === UNAVAILABLE}
.unit=${stateObj.attributes.unit_of_measurement}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-number-buttons>
`;
}
@@ -132,7 +143,7 @@ class HuiNumericInputCardFeature
@value-changed=${this._setValue}
.disabled=${stateObj.state === UNAVAILABLE}
.unit=${stateObj.attributes.unit_of_measurement}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -1,3 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { InputSelectEntity } from "../../../data/input_select";
@@ -16,6 +17,11 @@ import type {
type SelectOptionEntity = SelectEntity | InputSelectEntity;
const supportsSelectOptionsCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "select" || domain === "input_select";
};
export const supportsSelectOptionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -24,8 +30,7 @@ export const supportsSelectOptionsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "select" || domain === "input_select";
return supportsSelectOptionsCardFeatureFromState(stateObj);
};
@customElement("hui-select-options-card-feature")
@@ -49,7 +54,7 @@ class HuiSelectOptionsCardFeature
protected readonly _serviceAction = "select_option";
protected get _label(): string {
return this.hass!.localize("ui.card.select.option");
return this._localize("ui.card.select.option");
}
protected readonly _allowIconsStyle = false;
@@ -72,7 +77,7 @@ class HuiSelectOptionsCardFeature
}
protected _getOptions(): HuiModeSelectOption[] {
if (!this._stateObj || !this.hass) {
if (!this._stateObj) {
return [];
}
@@ -81,7 +86,7 @@ class HuiSelectOptionsCardFeature
this._config?.options
).map((option) => ({
value: option,
label: this.hass!.formatEntityState(this._stateObj!, option),
label: this._formatters.formatEntityState(this._stateObj!, option),
}));
}
@@ -98,9 +103,8 @@ class HuiSelectOptionsCardFeature
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsSelectOptionsCardFeature(this.hass, this.context)
this._stateObj &&
supportsSelectOptionsCardFeatureFromState(this._stateObj)
);
}
}
@@ -1,12 +1,27 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-control-slider";
import {
apiContext,
formattersContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HumidifierEntity } from "../../../data/humidifier";
import type { HomeAssistant } from "../../../types";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -14,6 +29,11 @@ import type {
TargetHumidityCardFeatureConfig,
} from "./types";
const supportsTargetHumidityCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier";
};
export const supportsTargetHumidityCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -22,8 +42,7 @@ export const supportsTargetHumidityCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier";
return supportsTargetHumidityCardFeatureFromState(stateObj);
};
@customElement("hui-target-humidity-card-feature")
@@ -31,22 +50,31 @@ class HuiTargetHumidityCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HumidifierEntity;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@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() private _config?: TargetHumidityCardFeatureConfig;
@state() private _targetHumidity?: number;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
HumidifierEntity | undefined;
}
static getStubConfig(): TargetHumidityCardFeatureConfig {
return {
type: "target-humidity",
@@ -60,17 +88,10 @@ class HuiTargetHumidityCardFeature
this._config = config;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._targetHumidity = this._stateObj!.attributes.humidity;
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._targetHumidity = this._stateObj.attributes.humidity;
}
}
@@ -92,7 +113,7 @@ class HuiTargetHumidityCardFeature
}
private _callService() {
this.hass!.callService("humidifier", "set_humidity", {
this._api.callService("humidifier", "set_humidity", {
entity_id: this._stateObj!.entity_id,
humidity: this._targetHumidity,
});
@@ -101,10 +122,9 @@ class HuiTargetHumidityCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsTargetHumidityCardFeature(this.hass, this.context)
!supportsTargetHumidityCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -117,12 +137,12 @@ class HuiTargetHumidityCardFeature
.step=${this._step}
.disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this._stateObj,
"humidity"
)}
unit="%"
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -1,8 +1,12 @@
import { consume } from "@lit/context";
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../common/const";
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@@ -13,10 +17,23 @@ import "../../../components/ha-control-button-group";
import "../../../components/ha-control-number-buttons";
import type { ClimateEntity } from "../../../data/climate";
import { ClimateEntityFeature } from "../../../data/climate";
import {
apiContext,
configContext,
formattersContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { FrontendLocaleData } from "../../../data/translation";
import type { WaterHeaterEntity } from "../../../data/water_heater";
import { WaterHeaterEntityFeature } from "../../../data/water_heater";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -26,14 +43,9 @@ import type {
type Target = "value" | "low" | "high";
export const supportsTargetTemperatureCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
const supportsTargetTemperatureCardFeatureFromState = (
stateObj: HassEntity
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
(domain === "climate" &&
@@ -47,27 +59,54 @@ export const supportsTargetTemperatureCardFeature = (
);
};
export const supportsTargetTemperatureCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
return supportsTargetTemperatureCardFeatureFromState(stateObj);
};
@customElement("hui-target-temperature-card-feature")
class HuiTargetTemperatureCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: WaterHeaterEntity | ClimateEntity;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@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 })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig?: HassConfig;
@state() private _config?: TargetTemperatureCardFeatureConfig;
@state() private _targetTemperature: Partial<Record<Target, number>> = {};
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
WaterHeaterEntity | ClimateEntity | undefined;
}
static getStubConfig(): TargetTemperatureCardFeatureConfig {
return {
type: "target-temperature",
@@ -81,34 +120,27 @@ class HuiTargetTemperatureCardFeature
this._config = config;
}
protected willUpdate(changedProp: PropertyValues<this>): void {
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (
(changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._targetTemperature = {
value: this._stateObj!.attributes.temperature,
low:
"target_temp_low" in this._stateObj!.attributes
? this._stateObj!.attributes.target_temp_low
: undefined,
high:
"target_temp_high" in this._stateObj!.attributes
? this._stateObj!.attributes.target_temp_high
: undefined,
};
}
if (changedProp.has("_stateObj") && this._stateObj) {
this._targetTemperature = {
value: this._stateObj.attributes.temperature,
low:
"target_temp_low" in this._stateObj.attributes
? this._stateObj.attributes.target_temp_low
: undefined,
high:
"target_temp_high" in this._stateObj.attributes
? this._stateObj.attributes.target_temp_high
: undefined,
};
}
}
private get _step() {
return (
this._stateObj!.attributes.target_temp_step ||
(this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
(this._hassConfig?.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}
@@ -143,14 +175,14 @@ class HuiTargetTemperatureCardFeature
private _callService(type: string) {
const domain = computeStateDomain(this._stateObj!);
if (type === "high" || type === "low") {
this.hass!.callService(domain, "set_temperature", {
this._api.callService(domain, "set_temperature", {
entity_id: this._stateObj!.entity_id,
target_temp_low: this._targetTemperature.low,
target_temp_high: this._targetTemperature.high,
});
return;
}
this.hass!.callService(domain, "set_temperature", {
this._api.callService(domain, "set_temperature", {
entity_id: this._stateObj!.entity_id,
temperature: this._targetTemperature.value,
});
@@ -186,10 +218,9 @@ class HuiTargetTemperatureCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsTargetTemperatureCardFeature(this.hass, this.context)
!supportsTargetTemperatureCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -213,12 +244,12 @@ class HuiTargetTemperatureCardFeature
.formatOptions=${options}
.target=${"value"}
.value=${this._stateObj.attributes.temperature}
.unit=${this.hass.config.unit_system.temperature}
.unit=${this._hassConfig?.unit_system.temperature}
.min=${this._min}
.max=${this._max}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this._stateObj,
"temperature"
)}
@@ -226,7 +257,7 @@ class HuiTargetTemperatureCardFeature
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale}
.locale=${this._locale}
>
</ha-control-number-buttons>
</ha-control-button-group>
@@ -245,7 +276,7 @@ class HuiTargetTemperatureCardFeature
.formatOptions=${options}
.target=${"low"}
.value=${this._targetTemperature.low}
.unit=${this.hass.config.unit_system.temperature}
.unit=${this._hassConfig?.unit_system.temperature}
.min=${this._min}
.max=${Math.min(
this._max,
@@ -253,7 +284,7 @@ class HuiTargetTemperatureCardFeature
)}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this._stateObj,
"target_temp_low"
)}
@@ -261,14 +292,14 @@ class HuiTargetTemperatureCardFeature
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale}
.locale=${this._locale}
>
</ha-control-number-buttons>
<ha-control-number-buttons
.formatOptions=${options}
.target=${"high"}
.value=${this._targetTemperature.high}
.unit=${this.hass.config.unit_system.temperature}
.unit=${this._hassConfig?.unit_system.temperature}
.min=${Math.max(
this._min,
this._targetTemperature.low ?? this._min
@@ -276,7 +307,7 @@ class HuiTargetTemperatureCardFeature
.max=${this._max}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
this._stateObj,
"target_temp_high"
)}
@@ -284,7 +315,7 @@ class HuiTargetTemperatureCardFeature
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale}
.locale=${this._locale}
>
</ha-control-number-buttons>
</ha-control-button-group>
@@ -295,15 +326,15 @@ class HuiTargetTemperatureCardFeature
<ha-control-button-group>
<ha-control-number-buttons
.disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${this.hass.config.unit_system.temperature}
.label=${this.hass.formatEntityAttributeName(
.unit=${this._hassConfig?.unit_system.temperature}
.label=${this._formatters.formatEntityAttributeName(
this._stateObj,
"temperature"
)}
style=${styleMap({
"--control-number-buttons-focus-color": stateColor,
})}
.locale=${this.hass.locale}
.locale=${this._locale}
>
</ha-control-number-buttons>
</ha-control-button-group>
@@ -8,19 +8,26 @@ import {
mdiVolumeHigh,
mdiVolumeOff,
} from "@mdi/js";
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-switch";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { forwardHaptic } from "../../../data/haptics";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -28,14 +35,7 @@ import type {
ToggleCardFeatureConfig,
} from "./types";
export const supportsToggleCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const supportsToggleCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return [
"switch",
@@ -47,6 +47,17 @@ export const supportsToggleCardFeature = (
].includes(domain);
};
export const supportsToggleCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
return supportsToggleCardFeatureFromState(stateObj);
};
const DOMAIN_ICONS: Record<string, { on: string; off: string }> = {
siren: {
on: mdiVolumeHigh,
@@ -64,18 +75,21 @@ const DOMAIN_ICONS: Record<string, { on: string; off: string }> = {
@customElement("hui-toggle-card-feature")
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ToggleCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: HassEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: ToggleCardFeatureConfig;
static getStubConfig(): ToggleCardFeatureConfig {
return {
@@ -109,7 +123,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
}
private async _callService(turnOn): Promise<void> {
if (!this.hass || !this._stateObj) {
if (!this._stateObj) {
return;
}
forwardHaptic(this, "light");
@@ -117,7 +131,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
const serviceDomain = stateDomain;
const service = turnOn ? "turn_on" : "turn_off";
await this.hass.callService(serviceDomain, service, {
await this._api.callService(serviceDomain, service, {
entity_id: this._stateObj.entity_id,
});
}
@@ -125,10 +139,9 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsToggleCardFeature(this.hass, this.context)
!supportsToggleCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -150,7 +163,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.common.turn_off")}
.label=${this._localize("ui.card.common.turn_off")}
@click=${this._turnOff}
.disabled=${this._stateObj.state === UNAVAILABLE}
class=${classMap({
@@ -163,7 +176,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
<ha-svg-icon .path=${offIcon}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.common.turn_on")}
.label=${this._localize("ui.card.common.turn_on")}
@click=${this._turnOn}
.disabled=${this._stateObj.state === UNAVAILABLE}
class=${classMap({
@@ -185,7 +198,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
.pathOff=${offIcon}
.checked=${isOn}
@change=${this._valueChanged}
.label=${this.hass.localize("ui.card.common.toggle")}
.label=${this._localize("ui.card.common.toggle")}
.disabled=${this._stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
@@ -1,16 +1,24 @@
import { consume } from "@lit/context";
import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { UpdateEntity } from "../../../data/update";
import { UpdateEntityFeature, updateIsInstalling } from "../../../data/update";
import { showUpdateBackupDialogParams } from "../../../dialogs/update_backup/show-update-backup-dialog";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +28,14 @@ import type {
export const DEFAULT_UPDATE_BACKUP_OPTION = "no";
const supportsUpdateActionsCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "update" &&
supportsFeature(stateObj, UpdateEntityFeature.INSTALL)
);
};
export const supportsUpdateActionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,11 +44,7 @@ export const supportsUpdateActionsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "update" &&
supportsFeature(stateObj, UpdateEntityFeature.INSTALL)
);
return supportsUpdateActionsCardFeatureFromState(stateObj);
};
@customElement("hui-update-actions-card-feature")
@@ -40,19 +52,21 @@ class HuiUpdateActionsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: UpdateActionsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: UpdateEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
UpdateEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: UpdateActionsCardFeatureConfig;
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-update-actions-card-feature-editor");
@@ -115,14 +129,14 @@ class HuiUpdateActionsCardFeature
backup = response;
}
this.hass!.callService("update", "install", {
this._api.callService("update", "install", {
entity_id: this._stateObj!.entity_id,
backup: backup,
});
}
private async _skip(): Promise<void> {
this.hass!.callService("update", "skip", {
this._api.callService("update", "skip", {
entity_id: this._stateObj!.entity_id,
});
}
@@ -130,10 +144,9 @@ class HuiUpdateActionsCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsUpdateActionsCardFeature(this.hass, this.context)
!supportsUpdateActionsCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -141,16 +154,14 @@ class HuiUpdateActionsCardFeature
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
.label=${this._localize("ui.dialogs.more_info_control.update.skip")}
@click=${this._skip}
.disabled=${this._skipDisabled}
>
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.update.install"
)}
@click=${this._install}
@@ -7,14 +7,21 @@ import {
mdiStop,
mdiTargetVariant,
} from "@mdi/js";
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { VacuumEntity } from "../../../data/vacuum";
import {
@@ -24,7 +31,7 @@ import {
canStop,
isCleaning,
} from "../../../data/vacuum";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -125,6 +132,14 @@ export const VACUUM_COMMANDS_BUTTONS: Record<
}),
};
const supportsVacuumCommandsCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "vacuum" &&
VACUUM_COMMANDS.some((c) => supportsVacuumCommand(stateObj, c))
);
};
export const supportsVacuumCommandsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -133,11 +148,7 @@ export const supportsVacuumCommandsCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "vacuum" &&
VACUUM_COMMANDS.some((c) => supportsVacuumCommand(stateObj, c))
);
return supportsVacuumCommandsCardFeatureFromState(stateObj);
};
@customElement("hui-vacuum-commands-card-feature")
@@ -145,19 +156,21 @@ class HuiVacuumCommandCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: VacuumCommandsCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: VacuumEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
VacuumEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: VacuumCommandsCardFeatureConfig;
static getStubConfig(): VacuumCommandsCardFeatureConfig {
return {
@@ -180,7 +193,7 @@ class HuiVacuumCommandCardFeature
private _onCommandTap(ev): void {
ev.stopPropagation();
const entry = (ev.target! as any).entry as VacuumButton;
this.hass!.callService("vacuum", entry.serviceName, {
this._api.callService("vacuum", entry.serviceName, {
entity_id: this._stateObj!.entity_id,
});
}
@@ -188,10 +201,9 @@ class HuiVacuumCommandCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsVacuumCommandsCardFeature(this.hass, this.context)
!supportsVacuumCommandsCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -209,7 +221,7 @@ class HuiVacuumCommandCardFeature
return html`
<ha-control-button
.entry=${button}
.label=${this.hass!.localize(
.label=${this._localize(
// @ts-ignore
`ui.dialogs.more_info_control.vacuum.${button.translationKey}`
)}
@@ -1,15 +1,23 @@
import { consume } from "@lit/context";
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-switch";
import "../../../components/ha-svg-icon";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import {
canClose,
@@ -18,7 +26,7 @@ import {
ValveEntityFeature,
type ValveEntity,
} from "../../../data/valve";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -26,6 +34,15 @@ import type {
ValveOpenCloseCardFeatureConfig,
} from "./types";
const supportsValveOpenCloseCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
(supportsFeature(stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(stateObj, ValveEntityFeature.CLOSE))
);
};
export const supportsValveOpenCloseCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -34,12 +51,7 @@ export const supportsValveOpenCloseCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
(supportsFeature(stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(stateObj, ValveEntityFeature.CLOSE))
);
return supportsValveOpenCloseCardFeatureFromState(stateObj);
};
@customElement("hui-valve-open-close-card-feature")
@@ -47,18 +59,21 @@ class HuiValveOpenCloseCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ValveOpenCloseCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: ValveEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _config?: ValveOpenCloseCardFeatureConfig;
static getStubConfig(): ValveOpenCloseCardFeatureConfig {
return {
@@ -74,13 +89,13 @@ class HuiValveOpenCloseCardFeature
}
private _onOpenValve(): void {
this.hass!.callService("valve", "open_valve", {
this._api.callService("valve", "open_valve", {
entity_id: this._stateObj!.entity_id,
});
}
private _onCloseValve(): void {
this.hass!.callService("valve", "close_valve", {
this._api.callService("valve", "close_valve", {
entity_id: this._stateObj!.entity_id,
});
}
@@ -97,7 +112,7 @@ class HuiValveOpenCloseCardFeature
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass!.callService("valve", "stop_valve", {
this._api.callService("valve", "stop_valve", {
entity_id: this._stateObj!.entity_id,
});
}
@@ -116,10 +131,9 @@ class HuiValveOpenCloseCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsValveOpenCloseCardFeature(this.hass, this.context)
!supportsValveOpenCloseCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -146,7 +160,7 @@ class HuiValveOpenCloseCardFeature
supportsFeature(this._stateObj, ValveEntityFeature.CLOSE)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.close_valve")}
.label=${this._localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this._stateObj)}
class=${classMap({
@@ -165,7 +179,7 @@ class HuiValveOpenCloseCardFeature
supportsFeature(this._stateObj, ValveEntityFeature.STOP)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.stop_valve")}
.label=${this._localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this._stateObj)}
>
@@ -178,7 +192,7 @@ class HuiValveOpenCloseCardFeature
supportsFeature(this._stateObj, ValveEntityFeature.OPEN)
? html`
<ha-control-button
.label=${this.hass.localize("ui.card.valve.open_valve")}
.label=${this._localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this._stateObj)}
class=${classMap({
@@ -203,7 +217,7 @@ class HuiValveOpenCloseCardFeature
.pathOff=${closedIcon}
.checked=${isOpen}
@change=${this._valueChanged}
.label=${this.hass.localize("ui.card.common.toggle")}
.label=${this._localize("ui.card.common.toggle")}
.disabled=${this._stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
@@ -1,18 +1,36 @@
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import {
consumeEntityState,
consumeLocalize,
} from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-slider";
import {
apiContext,
entitiesContext,
internationalizationContext,
} from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
import type { FrontendLocaleData } from "../../../data/translation";
import { ValveEntityFeature, type ValveEntity } from "../../../data/valve";
import type { HomeAssistant } from "../../../types";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantInternationalization,
} from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
@@ -20,6 +38,14 @@ import type {
ValvePositionCardFeatureConfig,
} from "./types";
const supportsValvePositionCardFeatureFromState = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
supportsFeature(stateObj, ValveEntityFeature.SET_POSITION)
);
};
export const supportsValvePositionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -28,11 +54,7 @@ export const supportsValvePositionCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "valve" &&
supportsFeature(stateObj, ValveEntityFeature.SET_POSITION)
);
return supportsValvePositionCardFeatureFromState(stateObj);
};
@customElement("hui-valve-position-card-feature")
@@ -40,20 +62,34 @@ class HuiValvePositionCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string;
@state() private _config?: ValvePositionCardFeatureConfig;
@state()
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
private _stateObj?: ValveEntity;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
}
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities!: HomeAssistant["entities"];
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale?: FrontendLocaleData;
@state() private _config?: ValvePositionCardFeatureConfig;
static getStubConfig(): ValvePositionCardFeatureConfig {
return {
@@ -71,10 +107,9 @@ class HuiValvePositionCardFeature
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsValvePositionCardFeature(this.hass, this.context)
!supportsValvePositionCardFeatureFromState(this._stateObj)
) {
return nothing;
}
@@ -108,14 +143,14 @@ class HuiValvePositionCardFeature
show-handle
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this._localize,
this._stateObj,
this.hass.entities,
this._entities,
"current_position"
)}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.valve.current_position}
.locale=${this.hass.locale}
.locale=${this._locale}
></ha-control-slider>
`;
}
@@ -124,7 +159,7 @@ class HuiValvePositionCardFeature
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("valve", "set_valve_position", {
this._api.callService("valve", "set_valve_position", {
entity_id: this._stateObj!.entity_id,
position: value,
});
@@ -1,4 +1,5 @@
import { mdiWaterBoiler } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
@@ -13,6 +14,13 @@ import type {
WaterHeaterOperationModesCardFeatureConfig,
} from "./types";
const supportsWaterHeaterOperationModesCardFeatureFromState = (
stateObj: HassEntity
) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "water_heater";
};
export const supportsWaterHeaterOperationModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
@@ -21,8 +29,7 @@ export const supportsWaterHeaterOperationModesCardFeature = (
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return domain === "water_heater";
return supportsWaterHeaterOperationModesCardFeatureFromState(stateObj);
};
@customElement("hui-water-heater-operation-modes-card-feature")
@@ -48,7 +55,7 @@ class HuiWaterHeaterOperationModeCardFeature
protected readonly _serviceAction = "set_operation_mode";
protected get _label(): string {
return this.hass!.localize("ui.card.water_heater.mode");
return this._localize("ui.card.water_heater.mode");
}
protected readonly _defaultStyle = "icons";
@@ -82,7 +89,7 @@ class HuiWaterHeaterOperationModeCardFeature
}
protected _getOptions() {
if (!this._stateObj || !this.hass) {
if (!this._stateObj) {
return [];
}
@@ -94,16 +101,15 @@ class HuiWaterHeaterOperationModeCardFeature
return filterModes(orderedModes, this._config?.operation_modes).map(
(mode) => ({
value: mode,
label: this.hass!.formatEntityState(this._stateObj!, mode),
label: this._formatters.formatEntityState(this._stateObj!, mode),
})
);
}
protected _isSupported(): boolean {
return !!(
this.hass &&
this.context &&
supportsWaterHeaterOperationModesCardFeature(this.hass, this.context)
this._stateObj &&
supportsWaterHeaterOperationModesCardFeatureFromState(this._stateObj)
);
}
}
+1 -1
View File
@@ -245,7 +245,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
// Side Effect used to update footer hass while keeping optimizations
if (this._footerElement) {
if (this._footerElement && "hass" in this._footerElement) {
this._footerElement.hass = this.hass;
}
+16 -5
View File
@@ -1,9 +1,12 @@
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../types";
import { configContext } from "../../../data/context";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { ErrorCardConfig } from "./types";
@@ -14,7 +17,13 @@ const ERROR_ICONS = {
@customElement("hui-error-card")
export class HuiErrorCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant;
@state()
@consumeLocalize()
private _localize?: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
private _hassConfig?: ContextType<typeof configContext>;
@property({ attribute: false }) public preview = false;
@@ -45,10 +54,12 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
const error =
this._config?.error ||
(this.severity === "warning" &&
this.hass?.localize("ui.errors.config.configuration_warning")) ||
this.hass?.localize("ui.errors.config.configuration_error");
this._localize?.("ui.errors.config.configuration_warning")) ||
this._localize?.("ui.errors.config.configuration_error");
const showTitle =
this.hass === undefined || this.hass?.user?.is_admin || this.preview;
this._hassConfig === undefined ||
this._hassConfig.user?.is_admin ||
this.preview;
const showMessage = this.preview;
return html`
@@ -68,9 +68,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
this._cards.forEach((card) => {
card.hass = this.hass;
});
if (this._errorCard) {
this._errorCard.hass = this.hass;
}
}
if (changedProperties.has("preview")) {
this._cards.forEach((card) => {
@@ -254,7 +254,7 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
protected shouldUpdate(changedProps: PropertyValues): boolean {
// Side Effect used to update footer hass while keeping optimizations
if (this._footerElement) {
if (this._footerElement && "hass" in this._footerElement) {
this._footerElement.hass = this.hass;
}
if (
@@ -1,13 +1,13 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement } from "lit/decorators";
import "../../../components/ha-alert";
import type { HomeAssistant } from "../../../types";
import "../cards/hui-error-card";
export const createEntityNotFoundWarning = (
hass: HomeAssistant,
hass: Pick<HomeAssistant, "config" | "localize">,
// left for backwards compatibility for custom cards
_entityId: string
) =>
@@ -17,10 +17,8 @@ export const createEntityNotFoundWarning = (
@customElement("hui-warning")
export class HuiWarning extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
protected render(): TemplateResult {
return html`<hui-error-card .hass=${this.hass} severity="warning"
return html`<hui-error-card severity="warning"
><slot></slot
></hui-error-card>`;
}
@@ -30,7 +30,9 @@ class EntityRowDirective extends Directive {
}
this._entityId = entityId;
this._name = name;
this._element.hass = hass;
if ("hass" in this._element) {
this._element.hass = hass;
}
return this._element;
}
}
+39 -9
View File
@@ -25,35 +25,65 @@ export const getMyRedirects = (): Redirects => ({
application_credentials: {
redirect: "/config/application_credentials",
},
tools_assist: {
redirect: "/config/tools/assist",
},
tools_debug: {
redirect: "/config/tools/debug",
},
tools_states: {
redirect: "/config/tools/state",
},
tools_actions: {
redirect: "/config/tools/action",
},
tools_perform_action: {
redirect: "/config/tools/action",
params: {
service: "string",
},
},
tools_template: {
redirect: "/config/tools/template",
},
tools_events: {
redirect: "/config/tools/event",
},
tools_statistics: {
redirect: "/config/tools/statistics",
},
tools_yaml: {
redirect: "/config/tools/yaml",
},
developer_assist: {
redirect: "/config/developer-tools/assist",
redirect: "/config/tools/assist",
},
developer_debug: {
redirect: "/config/developer-tools/debug",
redirect: "/config/tools/debug",
},
developer_states: {
redirect: "/config/developer-tools/state",
redirect: "/config/tools/state",
},
developer_services: {
redirect: "/config/developer-tools/action",
redirect: "/config/tools/action",
},
developer_call_service: {
redirect: "/config/developer-tools/action",
redirect: "/config/tools/action",
params: {
service: "string",
},
},
developer_template: {
redirect: "/config/developer-tools/template",
redirect: "/config/tools/template",
},
developer_events: {
redirect: "/config/developer-tools/event",
redirect: "/config/tools/event",
},
developer_statistics: {
redirect: "/config/developer-tools/statistics",
redirect: "/config/tools/statistics",
},
server_controls: {
redirect: "/config/developer-tools/yaml",
redirect: "/config/tools/yaml",
},
calendar: {
component: "calendar",
+51 -51
View File
@@ -1459,7 +1459,7 @@
"automations": "[%key:ui::panel::config::automation::caption%]",
"scenes": "[%key:ui::panel::config::scene::caption%]",
"scripts": "[%key:ui::panel::config::script::caption%]",
"developer_tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]",
"tools": "[%key:ui::panel::config::dashboard::tools::main%]",
"integrations": "[%key:ui::panel::config::integrations::caption%]",
"devices": "[%key:ui::panel::config::devices::caption%]",
"entities": "[%key:ui::panel::config::entities::caption%]"
@@ -1485,45 +1485,45 @@
"navigate_title": "Navigate",
"commands": {
"reload": {
"all": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::all%]",
"reload": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::reload%]",
"core": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::core%]",
"group": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::group%]",
"automation": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::automation%]",
"script": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::script%]",
"scene": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::scene%]",
"person": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::person%]",
"zone": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::zone%]",
"input_boolean": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_boolean%]",
"input_button": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_button%]",
"input_text": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_text%]",
"input_number": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_number%]",
"input_datetime": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_datetime%]",
"input_select": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_select%]",
"template": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::template%]",
"universal": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::universal%]",
"rest": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rest%]",
"command_line": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::command_line%]",
"filter": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filter%]",
"statistics": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::statistics%]",
"generic": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic%]",
"generic_thermostat": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic_thermostat%]",
"homekit": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::homekit%]",
"min_max": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::min_max%]",
"history_stats": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::history_stats%]",
"trend": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::trend%]",
"ping": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::ping%]",
"filesize": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filesize%]",
"telegram": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::telegram%]",
"smtp": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::smtp%]",
"mqtt": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::mqtt%]",
"rpi_gpio": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rpi_gpio%]",
"themes": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::themes%]"
"all": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::all%]",
"reload": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::reload%]",
"core": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::core%]",
"group": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::group%]",
"automation": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::automation%]",
"script": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::script%]",
"scene": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::scene%]",
"person": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::person%]",
"zone": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::zone%]",
"input_boolean": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_boolean%]",
"input_button": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_button%]",
"input_text": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_text%]",
"input_number": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_number%]",
"input_datetime": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_datetime%]",
"input_select": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_select%]",
"template": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::template%]",
"universal": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::universal%]",
"rest": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::rest%]",
"command_line": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::command_line%]",
"filter": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::filter%]",
"statistics": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::statistics%]",
"generic": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::generic%]",
"generic_thermostat": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::generic_thermostat%]",
"homekit": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::homekit%]",
"min_max": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::min_max%]",
"history_stats": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::history_stats%]",
"trend": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::trend%]",
"ping": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::ping%]",
"filesize": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::filesize%]",
"telegram": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::telegram%]",
"smtp": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::smtp%]",
"mqtt": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::mqtt%]",
"rpi_gpio": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::rpi_gpio%]",
"themes": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::themes%]"
},
"home_assistant_control": {
"perform_action": "{action} Home Assistant",
"restart": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::restart%]",
"stop": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::stop%]"
"restart": "[%key:ui::panel::config::tools::tabs::yaml::section::server_management::restart%]",
"stop": "[%key:ui::panel::config::tools::tabs::yaml::section::server_management::stop%]"
},
"types": {
"reload": "Reload",
@@ -1559,14 +1559,14 @@
"analytics": "[%key:ui::panel::config::analytics::caption%]",
"system_health": "[%key:ui::panel::config::system_health::caption%]",
"blueprint": "[%key:ui::panel::config::blueprint::caption%]",
"server_control": "[%key:ui::panel::config::developer-tools::tabs::yaml::title%]",
"server_control": "[%key:ui::panel::config::tools::tabs::yaml::title%]",
"system": "[%key:ui::panel::config::dashboard::system::main%]",
"apps": "Apps",
"app_store": "App store",
"app_info": "{app} info",
"shortcuts": "[%key:ui::panel::config::info::shortcuts%]",
"labs": "[%key:ui::panel::config::labs::caption%]",
"developer-tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]",
"tools": "[%key:ui::panel::config::dashboard::tools::main%]",
"matter": "[%key:ui::panel::config::dashboard::matter::main%]",
"zha": "[%key:ui::panel::config::dashboard::zha::main%]",
"zwave_js": "[%key:ui::panel::config::dashboard::zwave_js::main%]",
@@ -2154,12 +2154,12 @@
"data": "Additional data"
},
"template": {
"time": "[%key:ui::panel::config::developer-tools::tabs::templates::time%]",
"all_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::all_listeners%]",
"no_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::no_listeners%]",
"listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::listeners%]",
"entity": "[%key:ui::panel::config::developer-tools::tabs::templates::entity%]",
"domain": "[%key:ui::panel::config::developer-tools::tabs::templates::domain%]"
"time": "[%key:ui::panel::config::tools::tabs::templates::time%]",
"all_listeners": "[%key:ui::panel::config::tools::tabs::templates::all_listeners%]",
"no_listeners": "[%key:ui::panel::config::tools::tabs::templates::no_listeners%]",
"listeners": "[%key:ui::panel::config::tools::tabs::templates::listeners%]",
"entity": "[%key:ui::panel::config::tools::tabs::templates::entity%]",
"domain": "[%key:ui::panel::config::tools::tabs::templates::domain%]"
}
},
"options_flow": {
@@ -2638,9 +2638,9 @@
"main": "System",
"secondary": "Create backups, check logs, or reboot your system"
},
"developer_tools": {
"main": "Developer tools",
"secondary": "Tools to inspect and debug your system"
"tools": {
"main": "Tools",
"secondary": "Inspect and debug your system"
},
"about": {
"main": "About",
@@ -3786,7 +3786,7 @@
"companion_apps": "Companion apps"
}
},
"developer-tools": {
"tools": {
"tabs": {
"assist": {
"tab": "Assist",
@@ -4819,7 +4819,7 @@
"run_text_pipeline": "Run text pipeline",
"run_audio_pipeline": "Run audio pipeline",
"run_audio_with_wake": "Run audio pipeline with wake word detection",
"response": "[%key:ui::panel::config::developer-tools::tabs::actions::response%]",
"response": "[%key:ui::panel::config::tools::tabs::actions::response%]",
"send": "Send",
"continue_listening": "Continue listening for wake word",
"continue_talking": "Continue talking",
@@ -5120,7 +5120,7 @@
"type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]",
"type_scene_plural": "scenes",
"new_automation_setup_failed_title": "New {type} setup timed out",
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in the Tools panel. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.",
"new_automation_setup_timedout_success": "The server has responded and this has now set up successfully. You may now close this dialog.",
"item_pasted": "{item} pasted",
+73 -8
View File
@@ -167,14 +167,6 @@ test.describe("Panel navigation", () => {
});
});
test("navigates to developer-tools panel", async ({ page }) => {
// Since 2026.2 developer-tools is part of the config panel
await goToPanel(page, "/config/developer-tools");
await expect(
page.locator("ha-panel-config, developer-tools-main").first()
).toBeAttached({ timeout: PANEL_TIMEOUT });
});
test("navigates to profile panel", async ({ page }) => {
await goToPanel(page, "/profile");
await expect(
@@ -183,6 +175,79 @@ test.describe("Panel navigation", () => {
});
});
// ---------------------------------------------------------------------------
// Tools panel (formerly Developer tools)
// ---------------------------------------------------------------------------
/**
* Every tool sub-page reachable under /config/tools, mapped to the custom
* element tools-router mounts for it (see tools-router.ts). Asserting on the
* specific element proves the route actually rendered its tool, not just the
* shared ha-panel-tools shell.
*/
const TOOLS_SUBPAGES: { route: string; element: string }[] = [
{ route: "yaml", element: "tools-yaml-config" },
{ route: "state", element: "tools-state" },
{ route: "action", element: "tools-action" },
{ route: "template", element: "tools-template" },
{ route: "event", element: "tools-event" },
{ route: "statistics", element: "tools-statistics" },
{ route: "assist", element: "tools-assist" },
{ route: "debug", element: "tools-debug" },
];
test.describe("Tools panel", () => {
test("base path renders the tools panel", async ({ page }) => {
await goToPanel(page, "/config/tools");
await expect(page.locator("ha-panel-tools")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
for (const { route, element } of TOOLS_SUBPAGES) {
test(`renders the ${route} sub-page`, async ({ page }) => {
await goToPanel(page, `/config/tools/${route}`);
await expect(page.locator(element)).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
}
test("service is an alias for the action tool", async ({ page }) => {
await goToPanel(page, "/config/tools/service");
await expect(page.locator("tools-action")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
});
// ---------------------------------------------------------------------------
// Tools redirects (old developer-tools URLs)
// ---------------------------------------------------------------------------
test.describe("Tools redirects", () => {
// The panel moved from top-level /developer-tools (pre-2026.2) to
// /config/developer-tools (2026.2), then was renamed to /config/tools
// (2026.8). Both old locations must redirect to the new one, and deep links
// must keep their sub-page. See the updateRoute() redirect in
// src/layouts/home-assistant.ts.
for (const oldBase of ["/developer-tools", "/config/developer-tools"]) {
test(`redirects ${oldBase} to the tools panel`, async ({ page }) => {
await goToPanel(page, oldBase);
await expect(page.locator("ha-panel-tools")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
test(`redirects ${oldBase}/state to the state tool`, async ({ page }) => {
await goToPanel(page, `${oldBase}/state`);
await expect(page.locator("tools-state")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
}
});
// ---------------------------------------------------------------------------
// Lovelace
// ---------------------------------------------------------------------------
-7
View File
@@ -43,11 +43,4 @@ export const e2eTestPanels: Panels = {
config: null,
url_path: "profile",
},
"developer-tools": {
component_name: "developer-tools",
icon: "mdi:hammer",
title: "developer_tools",
config: null,
url_path: "developer-tools",
},
};
+1 -1
View File
@@ -54,7 +54,7 @@ export class HaTest extends HomeAssistantAppEl {
: scenarios.default;
const initial: Partial<MockHomeAssistant> = {
// Use the full panel map (history + config + developer-tools enabled)
// Use the full panel map (history + config enabled)
panels: e2eTestPanels,
panelUrl: (() => {
const path = window.location.pathname;
+5 -5
View File
@@ -8565,9 +8565,9 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-import-x@npm:4.17.0":
version: 4.17.0
resolution: "eslint-plugin-import-x@npm:4.17.0"
"eslint-plugin-import-x@npm:4.17.1":
version: 4.17.1
resolution: "eslint-plugin-import-x@npm:4.17.1"
dependencies:
"@typescript-eslint/types": "npm:^8.56.0"
comment-parser: "npm:^1.4.1"
@@ -8587,7 +8587,7 @@ __metadata:
optional: true
eslint-import-resolver-node:
optional: true
checksum: 10/143081e0a2cb418990d5d61c08ad4dd46f4f10dd7664939cc4be8454c2f51cd69134746d2d8b7534786f3a13857d176456bb0c1d1ffc9c168830b4ce93d2c0a8
checksum: 10/1cb95284765cf0ff937f7ab44cf965278939c25dedc72ac41d00d954f4c0bd607bcaffacdeb22a3796aa8f2775c63dd25f818c7f897f061f2339035826cbaf5c
languageName: node
linkType: hard
@@ -9817,7 +9817,7 @@ __metadata:
eslint: "npm:10.6.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.11"
eslint-plugin-import-x: "npm:4.17.0"
eslint-plugin-import-x: "npm:4.17.1"
eslint-plugin-lit: "npm:2.3.1"
eslint-plugin-lit-a11y: "npm:5.1.1"
eslint-plugin-unused-imports: "npm:4.4.1"