Compare commits

..

10 Commits

Author SHA1 Message Date
renovate[bot] 674755e430 Update dependency prettier to v3.9.3 (#52947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 15:14:39 +03:00
Franck Nijhof f84664909f Link the config pane help icon to the dedicated docs page (#52940)
For built-in triggers, conditions, and actions, the help icon in the editor
config pane (and Developer Tools) linked to the integration page. Point it at
the dedicated page for that specific trigger/condition/action instead, e.g.
/triggers/air_quality.co2_changed. Custom integrations keep their own
documentation URL.
2026-07-02 14:26:57 +03:00
Franck Nijhof 4fd631f229 Refresh the template tool documentation panel (#52941)
The About templates panel still pointed at the upstream Jinja2 docs and a
single extensions page. Rewrite the intro and link to the current templating
documentation instead: the learning guide (introduction, working with states,
debugging) and the searchable template functions reference.
2026-07-02 14:18:07 +03:00
renovate[bot] 18cf41b793 Update dependency @rsdoctor/rspack-plugin to v1.5.17 (#52943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:14:44 +03:00
renovate[bot] e28788cb95 Update dependency idb-keyval to v6.2.6 (#52944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:14:24 +03:00
Petar Petrov f81b43491d Add My links for infrared and radio frequency config panels (#52931) 2026-07-01 20:06:34 +02: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
61 changed files with 1721 additions and 1042 deletions
+4 -4
View File
@@ -102,7 +102,7 @@
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.5",
"idb-keyval": "6.2.6",
"intl-messageformat": "11.2.9",
"js-yaml": "5.2.0",
"leaflet": "1.9.4",
@@ -147,7 +147,7 @@
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.61.1",
"@rsdoctor/rspack-plugin": "1.5.16",
"@rsdoctor/rspack-plugin": "1.5.17",
"@rspack/core": "2.1.1",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -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",
@@ -198,7 +198,7 @@
"map-stream": "0.0.7",
"minify-literals": "2.0.2",
"pinst": "3.0.0",
"prettier": "3.9.1",
"prettier": "3.9.3",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.6",
"sinon": "22.0.0",
+2 -2
View File
@@ -525,10 +525,10 @@ export class HaServiceControl extends LitElement {
this._manifest
? html` <a
href=${
this._manifest.is_built_in
this._manifest.is_built_in && this._value?.action
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/actions/${this._value.action}`
)
: this._manifest.documentation
}
-27
View File
@@ -127,8 +127,6 @@ export class HaInput extends WaInputMixin(LitElement) {
@query("wa-input")
private _input?: WaInput;
private _startSlotResizeObserver?: ResizeObserver;
@state()
@consume({ context: internationalizationContext, subscribe: true })
protected i18n?: ContextType<typeof internationalizationContext>;
@@ -169,15 +167,9 @@ export class HaInput extends WaInputMixin(LitElement) {
// Wait for wa-input to finish its first render
await this._input?.updateComplete;
this._syncStartSlotWidth();
this._observeStartSlot();
}
}
public override disconnectedCallback(): void {
super.disconnectedCallback();
this._startSlotResizeObserver?.disconnect();
}
protected render() {
const hasLabelSlot = this.label
? false
@@ -299,25 +291,6 @@ export class HaInput extends WaInputMixin(LitElement) {
return nothing;
}
// Safari can report the start-slot width as 0 during the first render, which
// leaves the floating label overlapping the start icon (e.g. the magnify icon
// in ha-input-search). Re-sync whenever the wrapper's size changes
// (0 -> icon width, or hidden -> shown) so the label padding stays correct.
private _observeStartSlot() {
if (typeof ResizeObserver === "undefined") {
return;
}
const startEl = this._input?.shadowRoot?.querySelector('[part~="start"]');
if (!startEl) {
return;
}
this._startSlotResizeObserver?.disconnect();
this._startSlotResizeObserver = new ResizeObserver(() =>
this._syncStartSlotWidth()
);
this._startSlotResizeObserver.observe(startEl);
}
private _syncStartSlotWidth = () => {
const startEl = this._input?.shadowRoot?.querySelector(
'[part~="start"]'
+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) {
@@ -195,7 +195,7 @@ export class HaPlatformCondition extends LitElement {
this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/conditions/${this.condition.condition}`
)
: this._manifest.documentation
}
@@ -190,7 +190,7 @@ export class HaPlatformTrigger extends LitElement {
this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/triggers/${this.trigger.trigger}`
)
: this._manifest.documentation
}
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
@@ -40,6 +41,15 @@ For loop example getting entity values in the weather domain:
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`;
// key resolves the label/description translation keys; path is passed through
// documentationUrl().
const TEMPLATE_DOCS_LINKS: { key: string; path: string }[] = [
{ key: "docs_introduction", path: "/docs/templating/introduction/" },
{ key: "docs_states", path: "/docs/templating/states/" },
{ key: "docs_debugging", path: "/docs/templating/debugging/" },
{ key: "docs_functions", path: "/template-functions/" },
];
@customElement("developer-tools-template")
class HaPanelDevTemplate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -120,31 +130,36 @@ class HaPanelDevTemplate extends LitElement {
"ui.panel.config.developer-tools.tabs.templates.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.engine_info"
)}
</p>
<h3>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.learn_more"
)}
</h3>
<ul>
<li>
<a
href="https://jinja.palletsprojects.com/en/latest/templates/"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.jinja_documentation"
)}
</a>
</li>
<li>
<a
href=${documentationUrl(
this.hass,
"/docs/configuration/templating/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.template_extensions"
)}</a
>
</li>
${TEMPLATE_DOCS_LINKS.map(
(link) => html`
<li>
<a
href=${documentationUrl(this.hass, link.path)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
`ui.panel.config.developer-tools.tabs.templates.${link.key}` as LocalizeKeys
)}</a
>
<span class="link-description"
>${this.hass.localize(
`ui.panel.config.developer-tools.tabs.templates.${link.key}_description` as LocalizeKeys
)}</span
>
</li>
`
)}
</ul>
</div>
</ha-expansion-panel>
@@ -441,6 +456,17 @@ ${
margin-block-start: var(--ha-space-1);
margin-block-end: var(--ha-space-1);
}
.description > h3 {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
margin-block-end: var(--ha-space-1);
}
.description li {
margin-block-end: var(--ha-space-1);
}
.description .link-description {
color: var(--secondary-text-color);
}
.render-pane .card-content {
user-select: text;
+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;
}
}
+7
View File
@@ -149,6 +149,13 @@ export const getMyRedirects = (): Redirects => ({
component: "energy",
redirect: "/config/energy",
},
config_infrared: {
redirect: "/config/infrared",
},
config_radiofrequency: {
component: "radio_frequency",
redirect: "/config/radio-frequency",
},
config_ssdp: {
component: "ssdp",
redirect: "/config/ssdp",
+11 -3
View File
@@ -3904,7 +3904,9 @@
},
"templates": {
"title": "Template",
"description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.",
"description": "Templates let you generate dynamic content from your Home Assistant data, such as a notification that lists which lights are on, or a sensor whose value is calculated from several other entities.",
"engine_info": "Home Assistant uses the Jinja templating engine, extended with functions for working with your entities, areas, devices, and more. Write a template in the editor below and its result updates live as your states change.",
"learn_more": "Learn more",
"about": "About templates",
"editor": "Template editor",
"result": "Result",
@@ -3912,8 +3914,14 @@
"confirm_reset": "Do you want to reset your current template back to the demo template?",
"confirm_clear": "Do you want to clear your current template?",
"result_type": "Result type",
"jinja_documentation": "Jinja2 template documentation",
"template_extensions": "Home Assistant template extensions",
"docs_introduction": "Introduction to templating",
"docs_introduction_description": "Start here for a step-by-step guide.",
"docs_states": "Working with states",
"docs_states_description": "Read entity states and attributes in templates.",
"docs_debugging": "Debugging templates",
"docs_debugging_description": "Find and fix problems in your templates.",
"docs_functions": "Template functions reference",
"docs_functions_description": "Search every available function, filter, and test.",
"unknown_error_template": "Unknown error rendering template",
"time": "This template updates at the start of each minute.",
"all_listeners": "This template listens for all state changed events.",
+60 -60
View File
@@ -4618,22 +4618,22 @@ __metadata:
languageName: node
linkType: hard
"@rsdoctor/client@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/client@npm:1.5.16"
checksum: 10/dcda4e8034a296090b073102423050764e636f9801f2e5a5904e1f2744b1fe26d5f8202aed7a785c9fc809044e1aef5c4b6a16b63974caec79d1b5996ec35d34
"@rsdoctor/client@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/client@npm:1.5.17"
checksum: 10/0eb788455390a1b41aa31d982d93ceab3dd30671776e40e8a4ea3256b4713f6441066e079ff9a14413825e21d547b9b7d4ba52059f8995644e26724ff07bbf56
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/core@npm:1.5.16"
"@rsdoctor/core@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/core@npm:1.5.17"
dependencies:
"@rsbuild/plugin-check-syntax": "npm:^1.6.1"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/sdk": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/sdk": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
"@rspack/resolver": "npm:^0.2.8"
browserslist-load-config: "npm:^1.0.2"
es-toolkit: "npm:^1.47.0"
@@ -4641,60 +4641,60 @@ __metadata:
fs-extra: "npm:^11.1.1"
semver: "npm:^7.7.4"
source-map: "npm:^0.7.6"
checksum: 10/be7b03b5a5a8a9be47f94159469c35488f98046c99e2ccd7daed325c3dd2a8b21c654c12ac6d40c2775546efade373f429f630e8905cc17a5c9151978a0caaf9
checksum: 10/a797d5243d1d3f758d8b38cea1a3195345525c3158c4061f5e78d875fe2001198c8967587153059eb7c5ff764f27b4685ce13fd55a27dccdcfe7cd8061fb30c9
languageName: node
linkType: hard
"@rsdoctor/graph@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/graph@npm:1.5.16"
"@rsdoctor/graph@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/graph@npm:1.5.17"
dependencies:
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
es-toolkit: "npm:^1.47.0"
path-browserify: "npm:1.0.1"
source-map: "npm:^0.7.6"
checksum: 10/949e3a2cc48ccbb2d554becb2270c4df4b4fc8a6e10bd55bf9dc4d5f9a5fb2823c3e11a30dce890beaaa1b0ab0039bba1554ad0e7ddc5e2ea47641222d454633
checksum: 10/e58ed532ea8cc743e45dd66b678e1da3d48939fe711ebfade47834ffc581be6089d611d18c97c3e12010e2c609049cb325d07021a5dc51ef651314f8fe9f5741
languageName: node
linkType: hard
"@rsdoctor/rspack-plugin@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/rspack-plugin@npm:1.5.16"
"@rsdoctor/rspack-plugin@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/rspack-plugin@npm:1.5.17"
dependencies:
"@rsdoctor/core": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/sdk": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/core": "npm:1.5.17"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/sdk": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
peerDependencies:
"@rspack/core": "*"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/2bebf2b8dfc5ffde77b46b45fedc7d5d9b96f4fe3e5e1b3762bff5de6f278d9288fe6c361fa1df5bb17926a45ae3c6038e165d4f1f5e867724df0a530590b36a
checksum: 10/336bd813010a7c164770033ae5a30644bf165ce0dff250b9160c7c003401c214bfd96e528c5941c09834bb21949a4815c46ecae0ca4d9200b2b946b9c3164f8a
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/sdk@npm:1.5.16"
"@rsdoctor/sdk@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/sdk@npm:1.5.17"
dependencies:
"@rsdoctor/client": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/client": "npm:1.5.17"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
launch-editor: "npm:^2.13.2"
safer-buffer: "npm:2.1.2"
socket.io: "npm:4.8.1"
tapable: "npm:2.3.3"
checksum: 10/8a845468e13c66b93f9784c7887f7040b1df24f43e9304b50c3a7258c6b172c2bc3ca5f6e5e9d15801d1634e3e48f6bc78115a7a0242890548d111ae512caf7d
checksum: 10/d8a146a43726d61a9d7d2cfca7e2cd48c42a7ba28c9d280353f986f1647c0a33f05ec68a45af4f22104df8a8dae7dc14d621e56a15f11c3967d78c21216be234
languageName: node
linkType: hard
"@rsdoctor/types@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/types@npm:1.5.16"
"@rsdoctor/types@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/types@npm:1.5.17"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -4708,16 +4708,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/f470a7047474669bd466c9cee15b5ef3e4b854d4347e3dd4f251f1d1f92bd9b7b5e6863349f5d3550485c3484497a1e8be71cefc56e883c81f8a734960d1fc5e
checksum: 10/4767825ae55498e25d1dfbecc0aebe2685b67701dae004617f4d95a68b85f19f29afe75f72b8efeab4fe49185bb9a3c85dcfce8f99852c49d4ee37a4f6b7d888
languageName: node
linkType: hard
"@rsdoctor/utils@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/utils@npm:1.5.16"
"@rsdoctor/utils@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/utils@npm:1.5.17"
dependencies:
"@babel/code-frame": "npm:7.26.2"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.17"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-attributes: "npm:^1.9.5"
@@ -4731,7 +4731,7 @@ __metadata:
picocolors: "npm:^1.1.1"
rslog: "npm:^2.1.2"
strip-ansi: "npm:^6.0.1"
checksum: 10/d73062cc01f4e2def276d6515f2810f54bfd7f819f1d3a00dd884621f9c008eda4b31ae6e6d96efaec81d1bbad98f0f49cb77e14a028d5f53c97aca84b6b07d4
checksum: 10/7c9b4a3824de61f6254df50f80c5efe53df662f30cb07047afa956a9bc3917dc71bf0aa73c8edde7f73f9577a9cb051e1a81caaffa118db414a4b655223fe92a
languageName: node
linkType: hard
@@ -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
@@ -9771,7 +9771,7 @@ __metadata:
"@octokit/rest": "npm:22.0.1"
"@playwright/test": "npm:1.61.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.5.16"
"@rsdoctor/rspack-plugin": "npm:1.5.17"
"@rspack/core": "npm:2.1.1"
"@rspack/dev-server": "npm:2.1.0"
"@swc/helpers": "npm:0.5.23"
@@ -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"
@@ -9837,7 +9837,7 @@ __metadata:
home-assistant-js-websocket: "npm:9.6.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.5"
idb-keyval: "npm:6.2.6"
intl-messageformat: "npm:11.2.9"
js-yaml: "npm:5.2.0"
jsdom: "npm:29.1.1"
@@ -9860,7 +9860,7 @@ __metadata:
node-vibrant: "npm:4.0.4"
object-hash: "npm:3.0.0"
pinst: "npm:3.0.0"
prettier: "npm:3.9.1"
prettier: "npm:3.9.3"
punycode: "npm:2.3.1"
qr-scanner: "npm:1.4.2"
qrcode: "npm:1.5.4"
@@ -10056,10 +10056,10 @@ __metadata:
languageName: node
linkType: hard
"idb-keyval@npm:6.2.5":
version: 6.2.5
resolution: "idb-keyval@npm:6.2.5"
checksum: 10/ac645882b3258ff07347d085baab91b871bac7be4f46ff8e20a7c036c2df35d3f695a30050009f27237b99045203568f2a842a35295a48f9b815959ee51a347e
"idb-keyval@npm:6.2.6":
version: 6.2.6
resolution: "idb-keyval@npm:6.2.6"
checksum: 10/8d0f8b9bd5eead685731a900510095dbc58936968739755bfd1de1c69a710daa5eb2b5cf185d0a7c7e9ce1daf4544fa5f58a2c7a37258a6826dd40f9e2614245
languageName: node
linkType: hard
@@ -12746,12 +12746,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:3.9.1":
version: 3.9.1
resolution: "prettier@npm:3.9.1"
"prettier@npm:3.9.3":
version: 3.9.3
resolution: "prettier@npm:3.9.3"
bin:
prettier: bin/prettier.cjs
checksum: 10/1b4317674aa9e90ff79c347fd19f91bb305df98b3122e7131d6815291707781305c45a13cd982474c2f74ed748f2fd0a9aa094f9856609ed1b6f092de8152058
checksum: 10/2aa4232a7ae2204a6d0758e8083117509f13a499ae49f87ed8e4a9c15967083f400e4e189a64948de60987974d3441f4b5e5110e7a3e56f9b83f4e2904cef376
languageName: node
linkType: hard