Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten 8d7ce043d9 Hide "for" on state condition when matching an attribute
The legacy state condition measures the `for` duration against
`last_changed`, which only updates on state changes, not attribute
changes. So `for` together with `attribute` is unreliable and core is
moving to reject it. Omit the `for` field when an attribute is selected,
drop any lingering duration value, and explain why via a helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:31:06 +02:00
140 changed files with 1719 additions and 3966 deletions
@@ -1,190 +0,0 @@
name: Pull request standards
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
types:
- opened
- edited
- reopened
- ready_for_review
branches:
- dev
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check:
name: Check pull request follows contribution standards
runs-on: ubuntu-latest
permissions:
pull-requests: write # To label and comment on pull requests
steps:
- name: Check pull request standards
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
if (pr.user.type === "Bot") {
core.info(`Skipping bot author: ${pr.user.login}`);
return;
}
if (pr.draft) {
core.info("Skipping draft pull request");
return;
}
try {
await github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: pr.user.login,
});
core.info(`Skipping organization member: ${pr.user.login}`);
return;
} catch (error) {
core.info(`${pr.user.login} is not an organization member, checking standards`);
}
const label = "Needs Template";
const marker = "<!-- pr-standards-check -->";
const { owner, repo } = context.repo;
const issue_number = pr.number;
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
const normalized = body.toLowerCase();
// Ignore 404s from mutations that race manual edits or cancelled runs.
const ignoreMissing = async (fn) => {
try {
await fn();
} catch (error) {
if (error.status === 404) {
core.info("Target already removed, nothing to do");
return;
}
throw error;
}
};
// Hide/restore our comment via GraphQL (REST cannot minimize).
const setMinimized = async (subjectId, minimized) => {
const mutation = minimized
? `mutation($id: ID!) {
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
clientMutationId
}
}`
: `mutation($id: ID!) {
unminimizeComment(input: { subjectId: $id }) {
clientMutationId
}
}`;
try {
await github.graphql(mutation, { id: subjectId });
} catch (error) {
core.info(
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
);
}
};
// Content of a "## <name>" section, or null when the heading is absent.
const section = (name) => {
const match = body.match(
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
);
return match ? match[1] : null;
};
const problems = [];
const requiredHeadings = [
"## proposed change",
"## type of change",
"## checklist",
];
if (requiredHeadings.some((h) => !normalized.includes(h))) {
problems.push(
"Use the pull request template without removing its sections."
);
}
const typeOfChange = section("type of change");
if (typeOfChange !== null) {
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
if (ticked !== 1) {
problems.push(
'Select exactly one option under "Type of change".'
);
}
}
const proposedChange = section("proposed change");
if (proposedChange !== null && proposedChange.trim().length === 0) {
problems.push('Describe your changes under "Proposed change".');
}
const isValid = problems.length === 0;
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number, per_page: 100 }
);
const existing = comments.find((c) => c.body.includes(marker));
const hasLabel = pr.labels.some((l) => l.name === label);
if (isValid) {
core.info("Pull request standards met");
if (hasLabel) {
await ignoreMissing(() =>
github.rest.issues.removeLabel({
owner, repo, issue_number, name: label,
})
);
}
if (existing) {
await setMinimized(existing.node_id, true);
}
return;
}
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
if (!hasLabel) {
await github.rest.issues.addLabels({
owner, repo, issue_number, labels: [label],
});
}
const message =
`${marker}\n` +
`Hey @${pr.user.login}!\n\n` +
`Thank you for your contribution! To help reviewers, please update ` +
`this pull request to follow our pull request standards:\n\n` +
problems.map((p) => `- ${p}`).join("\n") +
`\n\n` +
`Please complete the ` +
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
`for more on creating a great pull request (see point 6).`;
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body: message,
});
await setMinimized(existing.node_id, false);
} else {
await github.rest.issues.createComment({
owner, repo, issue_number, body: message,
});
}
// Fail this check so it can block the PR from being merged
core.setFailed(
`Pull request standards not met:\n- ${problems.join("\n- ")}`
);
-9
View File
@@ -110,15 +110,6 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
"media_player",
]);
/** Domains that use a timestamp for state. */
export const TIMESTAMP_STATE_DOMAINS = [
"button",
"infrared",
"input_button",
"radio_frequency",
"scene",
];
/** Temperature units. */
export const UNIT_C = "°C";
export const UNIT_F = "°F";
+4 -5
View File
@@ -3,20 +3,19 @@ import type { FrontendLocaleData } from "../../data/translation";
import { selectUnit } from "../util/select-unit";
const formatRelTimeMem = memoizeOne(
(locale: FrontendLocaleData, style: Intl.RelativeTimeFormatStyle) =>
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style })
(locale: FrontendLocaleData) =>
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
);
export const relativeTime = (
from: Date,
locale: FrontendLocaleData,
to?: Date,
includeTense = true,
style: Intl.RelativeTimeFormatStyle = "long"
includeTense = true
): string => {
const diff = selectUnit(from, to, locale);
if (includeTense) {
return formatRelTimeMem(locale, style).format(diff.value, diff.unit);
return formatRelTimeMem(locale).format(diff.value, diff.unit);
}
return Intl.NumberFormat(locale.language, {
style: "unit",
+23
View File
@@ -0,0 +1,23 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "./supports-feature";
export type FeatureClassNames<T extends number = number> = Partial<
Record<T, string>
>;
// Expects classNames to be an object mapping feature-bit -> className
export const featureClassNames = (
stateObj: HassEntity,
classNames: FeatureClassNames
) => {
if (!stateObj || !stateObj.attributes.supported_features) {
return "";
}
return Object.keys(classNames)
.map((feature) =>
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
)
.filter((attr) => attr !== "")
.join(" ");
};
-48
View File
@@ -1,48 +0,0 @@
import { AITaskEntityFeature } from "../../data/ai_task";
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
import { CalendarEntityFeature } from "../../data/calendar";
import { ClimateEntityFeature } from "../../data/climate";
import { ConversationEntityFeature } from "../../data/conversation";
import { CoverEntityFeature } from "../../data/cover";
import { FanEntityFeature } from "../../data/fan";
import { HumidifierEntityFeature } from "../../data/humidifier";
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
import { LightEntityFeature } from "../../data/light";
import { LockEntityFeature } from "../../data/lock";
import { MediaPlayerEntityFeature } from "../../data/media-player";
import { TodoListEntityFeature } from "../../data/todo";
import { UpdateEntityFeature } from "../../data/update";
import { VacuumEntityFeature } from "../../data/vacuum";
import { ValveEntityFeature } from "../../data/valve";
import { WaterHeaterEntityFeature } from "../../data/water_heater";
import { WeatherEntityFeature } from "../../data/weather";
export type FeatureEnum = Record<string | number, string | number>;
const DOMAIN_ENUMS = {
ai_task: AITaskEntityFeature,
alarm_control_panel: AlarmControlPanelEntityFeature,
assist_satellite: AssistSatelliteEntityFeature,
calendar: CalendarEntityFeature,
climate: ClimateEntityFeature,
conversation: ConversationEntityFeature,
cover: CoverEntityFeature,
fan: FanEntityFeature,
humidifier: HumidifierEntityFeature,
lawn_mower: LawnMowerEntityFeature,
light: LightEntityFeature,
lock: LockEntityFeature,
media_player: MediaPlayerEntityFeature,
todo: TodoListEntityFeature,
update: UpdateEntityFeature,
vacuum: VacuumEntityFeature,
valve: ValveEntityFeature,
water_heater: WaterHeaterEntityFeature,
weather: WeatherEntityFeature,
};
export function getFeatures(domain: string): FeatureEnum | undefined {
const enumObj = DOMAIN_ENUMS[domain] as FeatureEnum;
return enumObj;
}
+2
View File
@@ -17,6 +17,8 @@ export type LocalizeKeys =
| `ui.common.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.selectors.file.${string}`
| `ui.components.logbook.messages.detected_device_classes.${string}`
| `ui.components.logbook.messages.cleared_device_classes.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
@@ -16,12 +16,14 @@ interface CacheResult<T> {
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCachePromiseFunc = async <T, H = HomeAssistant>(
export const timeCachePromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: H, ...args: any[]) => Promise<T>,
generateCacheKey: ((hass: H, lastResult: T) => unknown) | undefined,
hass: H,
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
generateCacheKey:
| ((hass: HomeAssistant, lastResult: T) => unknown)
| undefined,
hass: HomeAssistant,
...args: any[]
): Promise<T> => {
const anyHass = hass as any;
@@ -1,12 +1,5 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-svg-icon";
import {
mdiAlertCircle,
mdiCheckCircle,
mdiCloseCircle,
mdiHelpCircle,
} from "@mdi/js";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -26,59 +19,46 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
private get _iconPath() {
switch (this.state) {
case "pass":
return mdiCheckCircle;
case "fail":
return mdiCloseCircle;
case "invalid":
return mdiAlertCircle;
default:
return mdiHelpCircle;
}
}
protected render() {
return html`
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
</div>
<div
id="indicator"
role="status"
tabindex="0"
aria-label=${this.label}
></div>
`;
}
static styles = css`
:host {
position: absolute;
top: -8px;
inset-inline-end: -8px;
top: -5px;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 16px;
height: 16px;
display: grid;
place-items: center;
width: 10px;
height: 10px;
border-radius: var(--ha-border-radius-circle);
border: var(--ha-border-width-md) solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
#indicator ha-svg-icon {
width: 16px;
height: 16px;
--mdc-icon-size: 16px;
}
:host([state="pass"]) #indicator {
color: var(--ha-color-green-60);
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
}
:host([state="fail"]) #indicator {
color: var(--ha-color-orange-60);
border-color: var(--ha-color-orange-60);
}
:host([state="invalid"]) #indicator {
color: var(--ha-color-red-60);
border-color: var(--ha-color-red-60);
}
:host([state="unknown"]) #indicator {
color: var(--ha-color-neutral-60);
border-color: var(--ha-color-neutral-60);
}
`;
}
+3 -5
View File
@@ -79,11 +79,9 @@ function computeTimelineEnumColor(
const domain = computeStateDomain(stateObj);
const states =
FIXED_DOMAIN_STATES[domain] ||
((domain === "sensor" && stateObj.attributes.device_class === "enum") ||
domain === "select" ||
domain === "input_select"
? stateObj.attributes.options
: undefined) ||
(domain === "sensor" &&
stateObj.attributes.device_class === "enum" &&
stateObj.attributes.options) ||
[];
const idx = states.indexOf(state);
if (idx === -1) {
+20 -46
View File
@@ -1,4 +1,3 @@
import { consume } from "@lit/context";
import {
mdiAlertCircle,
mdiChevronDown,
@@ -11,9 +10,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { supportsFeature } from "../common/entity/supports-feature";
import type { LocalizeFunc } from "../common/translations/localize";
import {
runAssistPipeline,
type AssistPipeline,
@@ -21,19 +18,10 @@ import {
type ConversationChatLogToolResultDelta,
type PipelineRunEvent,
} from "../data/assist_pipeline";
import {
configContext,
connectionContext,
statesContext,
} from "../data/context";
import { ConversationEntityFeature } from "../data/conversation";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { haStyleScrollbar } from "../resources/styles";
import type {
HomeAssistant,
HomeAssistantConfig,
HomeAssistantConnection,
} from "../types";
import type { HomeAssistant } from "../types";
import { AudioRecorder } from "../util/audio-recorder";
import { documentationUrl } from "../util/documentation-url";
import "./ha-alert";
@@ -59,6 +47,8 @@ interface AssistMessage {
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public pipeline?: AssistPipeline;
@property({ type: Boolean, attribute: "disable-speech" })
@@ -81,22 +71,6 @@ export class HaAssistChat extends LitElement {
@state() private _processing = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HomeAssistant["states"];
@state()
@consume({ context: configContext, subscribe: true })
private _config!: HomeAssistantConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: HomeAssistantConnection;
private _conversationId: string | null = null;
private _audioRecorder?: AudioRecorder;
@@ -112,7 +86,7 @@ export class HaAssistChat extends LitElement {
this._conversation = [
{
who: "hass",
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
thinking: "",
tool_calls: {},
},
@@ -150,9 +124,9 @@ export class HaAssistChat extends LitElement {
const controlHA = !this.pipeline
? false
: this.pipeline.prefer_local_intents ||
(this._states[this.pipeline.conversation_engine]
(this.hass.states[this.pipeline.conversation_engine]
? supportsFeature(
this._states[this.pipeline.conversation_engine],
this.hass.states[this.pipeline.conversation_engine],
ConversationEntityFeature.CONTROL
)
: true);
@@ -165,7 +139,7 @@ export class HaAssistChat extends LitElement {
? nothing
: html`
<ha-alert>
${this._localize(
${this.hass.localize(
"ui.dialogs.voice_command.conversation_no_control"
)}
</ha-alert>
@@ -206,7 +180,7 @@ export class HaAssistChat extends LitElement {
.path=${mdiCommentProcessingOutline}
></ha-svg-icon>
<span class="thinking-label">
${this._localize(
${this.hass.localize(
"ui.dialogs.voice_command.show_details"
)}
</span>
@@ -277,7 +251,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
id="message-input"
@keyup=${this._handleKeyUp}
@input=${this._handleInput}
.label=${this._localize(`ui.dialogs.voice_command.input_label`)}
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
>
<div slot="end">
${this._showSendButton || !supportsSTT
@@ -287,7 +261,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
.path=${mdiSend}
@click=${this._handleSendMessage}
.disabled=${this._processing}
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.voice_command.send_text"
)}
>
@@ -308,7 +282,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
.path=${mdiMicrophone}
@click=${this._handleListeningButton}
.disabled=${this._processing}
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.voice_command.start_listening"
)}
>
@@ -417,21 +391,21 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
text:
// New lines matter for messages
// prettier-ignore
html`${this._localize(
html`${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_browser"
)}
${this._localize(
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this._config,
this.hass,
"/docs/configuration/securing/#remote-access"
)}
>${this._localize(
>${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}</a>`,
}
@@ -469,7 +443,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
try {
const unsub = await runAssistPipeline(
this._connection,
this.hass,
(event: PipelineRunEvent) => {
if (event.type === "run-start") {
this._stt_binary_handler_id =
@@ -565,7 +539,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
}
private _sendAudioChunk(chunk: Int16Array) {
this._connection.connection.socket!.binaryType = "arraybuffer";
this.hass.connection.socket!.binaryType = "arraybuffer";
// eslint-disable-next-line eqeqeq
if (this._stt_binary_handler_id == undefined) {
@@ -576,7 +550,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
data[0] = this._stt_binary_handler_id;
data.set(new Uint8Array(chunk.buffer), 1);
this._connection.connection.socket!.send(data);
this.hass.connection.socket!.send(data);
}
private _unloadAudio = () => {
@@ -596,7 +570,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
hassMessageProcesser.addMessage();
try {
const unsub = await runAssistPipeline(
this._connection,
this.hass,
(event) => {
if (event.type.startsWith("intent-")) {
hassMessageProcesser.processEvent(event);
@@ -619,7 +593,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
);
} catch {
hassMessageProcesser.setError(
this._localize("ui.dialogs.voice_command.error")
this.hass.localize("ui.dialogs.voice_command.error")
);
} finally {
this._processing = false;
+6 -24
View File
@@ -1,20 +1,16 @@
import { consume } from "@lit/context";
import type { ContextType } 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 { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import {
configContext,
connectionContext,
entitiesContext,
} from "../data/context";
import { attributeIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-attribute-icon")
export class HaAttributeIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public attribute?: string;
@@ -23,18 +19,6 @@ export class HaAttributeIcon extends LitElement {
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities?: ContextType<typeof entitiesContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -44,14 +28,12 @@ export class HaAttributeIcon extends LitElement {
return nothing;
}
if (!this._config || !this._connection || !this._entities) {
if (!this.hass) {
return nothing;
}
const icon = attributeIcon(
this._config.config,
this._connection.connection,
this._entities,
this.hass,
this.stateObj,
this.attribute,
this.attributeValue
+12 -11
View File
@@ -1,12 +1,10 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatNumber } from "../common/number/format_number";
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
import { internationalizationContext } from "../data/context";
import type { HomeAssistant } from "../types";
@customElement("ha-big-number")
export class HaBigNumber extends LitElement {
@@ -17,16 +15,17 @@ export class HaBigNumber extends LitElement {
@property({ attribute: "unit-position" })
public unitPosition: "top" | "bottom" = "top";
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false })
public formatOptions: Intl.NumberFormatOptions = {};
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
protected render() {
const locale = this._i18n!.locale;
const formatted = formatNumber(this.value, locale, this.formatOptions);
const formatted = formatNumber(
this.value,
this.hass?.locale,
this.formatOptions
);
const [integer] = formatted.includes(".")
? formatted.split(".")
: formatted.split(",");
@@ -34,7 +33,9 @@ export class HaBigNumber extends LitElement {
const temperatureDecimal = formatted.replace(integer, "");
const formattedValue = `${this.value}${
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
this.unit
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
: ""
}`;
const unitBottom = this.unitPosition === "bottom";
+9 -3
View File
@@ -101,9 +101,15 @@ export class HaLabelsPicker extends LitElement {
language: string
) =>
value
?.map((id) => labels?.find((label) => label.label_id === id))
.filter((label): label is LabelRegistryEntry => label !== undefined)
.sort((a, b) => stringCompare(a.name, b.name, language))
?.map(
(id) =>
labels?.find((label) => label.label_id === id) || {
label_id: id,
name: id,
color: "rgba(var(--rgb-primary-text-color), 0.15)",
}
)
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
.map((label) => ({
...label,
style: getLabelColorStyle(label.color),
+8 -20
View File
@@ -12,8 +12,6 @@ import type { HomeAssistantInternationalization } from "../types";
class HaRelativeTime extends ReactiveElement {
@property({ attribute: false }) public datetime?: string | Date;
@property() public format: Intl.RelativeTimeFormatStyle = "long";
@property({ type: Boolean }) public capitalize = false;
@state()
@@ -38,15 +36,13 @@ class HaRelativeTime extends ReactiveElement {
return this;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._updateRelative();
}
protected update(changedProps: PropertyValues<this>) {
super.update(changedProps);
if (changedProps.has("datetime")) {
if (this.datetime) {
this._startInterval();
} else {
this._clearInterval();
}
}
this._updateRelative();
}
@@ -70,23 +66,15 @@ class HaRelativeTime extends ReactiveElement {
}
if (!this.datetime) {
this.textContent = this._i18n.localize(
"ui.components.relative_time.never"
);
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
} else {
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(
date,
this._i18n.locale,
undefined,
true,
this.format
);
this.textContent = this.capitalize
const relTime = relativeTime(date, this._i18n.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
}
@@ -1,32 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-time-format-picker";
@customElement("ha-selector-ui_time_format")
export class HaSelectorUiTimeFormat extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`
<ha-time-format-picker
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
>
</ha-time-format-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
}
}
@@ -67,7 +67,6 @@ const LOAD_ELEMENTS = {
ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"),
ui_state_content: () => import("./ha-selector-ui-state-content"),
ui_time_format: () => import("./ha-selector-ui-time-format"),
};
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
-67
View File
@@ -1,67 +0,0 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import "./ha-select";
import { TIMESTAMP_RENDERING_FORMATS } from "../panels/lovelace/components/types";
@customElement("ha-time-format-picker")
export class HaTimeFormatPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
private _options = memoizeOne((localize: LocalizeFunc) =>
[{ label: localize("ui.common.auto"), value: "auto" }].concat(
TIMESTAMP_RENDERING_FORMATS.map((format) => ({
label:
localize(`ui.components.time-format-picker.formats.${format}`) ||
format,
value: format,
}))
)
);
protected render() {
return html`
<ha-select
.label=${this.label ?? ""}
.value=${this.value || "auto"}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
@selected=${this._selectChanged}
.options=${this._options(this._localize)}
>
</ha-select>
`;
}
private _selectChanged(ev) {
ev.stopPropagation();
if (ev.detail?.value === "auto" && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
return;
}
fireEvent(this, "value-changed", {
value: ev.detail.value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-time-format-picker": HaTimeFormatPicker;
}
}
+1
View File
@@ -26,6 +26,7 @@ export class HaTraceLogbook extends LitElement {
return this.logbookEntries.length
? html`
<ha-logbook-renderer
relative-time
.hass=${this.hass}
.entries=${this.logbookEntries}
.narrow=${this.narrow}
@@ -388,6 +388,7 @@ export class HaTracePathDetails extends LitElement {
return entries.length
? html`
<ha-logbook-renderer
relative-time
.hass=${this.hass}
.entries=${entries}
.narrow=${this.narrow}
+2 -2
View File
@@ -18,7 +18,7 @@ import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { fullEntitiesContext } from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LogbookEntry } from "../../data/logbook";
import { localizeTriggerSource } from "../../data/logbook";
import { localizeTriggerDescription } from "../../data/logbook";
import type {
ChooseAction,
IfAction,
@@ -333,7 +333,7 @@ class ActionRenderer {
: "other",
alias: triggerStep.changed_variables.trigger?.alias,
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
trigger: localizeTriggerSource(
trigger: localizeTriggerDescription(
this.hass.localize,
this.trace.trigger
),
+1 -1
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
import type { Selector } from "./selector";
export enum AITaskEntityFeature {
export const enum AITaskEntityFeature {
GENERATE_DATA = 1,
SUPPORT_ATTACHMENTS = 2,
GENERATE_IMAGE = 4,
+1 -1
View File
@@ -18,7 +18,7 @@ import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
export enum AlarmControlPanelEntityFeature {
export const enum AlarmControlPanelEntityFeature {
ARM_HOME = 1,
ARM_AWAY = 2,
ARM_NIGHT = 4,
+2 -5
View File
@@ -338,7 +338,7 @@ export const runDebugAssistPipeline = (
};
export const runAssistPipeline = (
hass: Pick<HomeAssistant, "connection">,
hass: HomeAssistant,
callback: (event: PipelineRunEvent) => void,
options: PipelineRunOptions
) =>
@@ -379,10 +379,7 @@ export const listAssistPipelines = (hass: HomeAssistant) =>
type: "assist_pipeline/pipeline/list",
});
export const getAssistPipeline = (
hass: Pick<HomeAssistant, "callWS">,
pipeline_id?: string
) =>
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/get",
pipeline_id,
+1 -1
View File
@@ -3,7 +3,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export enum AssistSatelliteEntityFeature {
export const enum AssistSatelliteEntityFeature {
ANNOUNCE = 1,
}
+1 -1
View File
@@ -377,7 +377,7 @@ export const expandConditionWithShorthand = (
};
export const triggerAutomationActions = (
hass: Pick<HomeAssistant, "callService">,
hass: HomeAssistant,
entityId: string
) => {
hass.callService("automation", "trigger", {
+1 -1
View File
@@ -181,7 +181,7 @@ export interface RestoreBackupParams {
restore_homeassistant?: boolean;
}
export const fetchBackupConfig = (hass: Pick<HomeAssistant, "callWS">) =>
export const fetchBackupConfig = (hass: HomeAssistant) =>
hass.callWS<{ config: BackupConfig }>({ type: "backup/config/info" });
export const updateBackupConfig = (
+1 -1
View File
@@ -54,7 +54,7 @@ export enum RecurrenceRange {
THISANDFUTURE = "THISANDFUTURE",
}
export enum CalendarEntityFeature {
export const enum CalendarEntityFeature {
CREATE_EVENT = 1,
DELETE_EVENT = 2,
UPDATE_EVENT = 4,
+1 -1
View File
@@ -68,7 +68,7 @@ export type ClimateEntity = HassEntityBase & {
};
};
export enum ClimateEntityFeature {
export const enum ClimateEntityFeature {
TARGET_TEMPERATURE = 1,
TARGET_TEMPERATURE_RANGE = 2,
TARGET_HUMIDITY = 4,
+1 -1
View File
@@ -1,7 +1,7 @@
import { ensureArray } from "../common/array/ensure-array";
import type { HomeAssistant } from "../types";
export enum ConversationEntityFeature {
export const enum ConversationEntityFeature {
CONTROL = 1,
}
+4 -4
View File
@@ -4,10 +4,10 @@ import type {
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistantFormatters } from "../types";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export enum CoverEntityFeature {
export const enum CoverEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
@@ -122,7 +122,7 @@ export interface CoverEntity extends HassEntityBase {
export function computeCoverPositionStateDisplay(
stateObj: CoverEntity,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
@@ -133,7 +133,7 @@ export function computeCoverPositionStateDisplay(
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? formatEntityAttributeValue(
? hass.formatEntityAttributeValue(
stateObj,
// Always use position as it's the same formatting as tilt position
"current_position",
+3 -3
View File
@@ -1,14 +1,14 @@
import type { HassEntityBase } from "home-assistant-js-websocket";
import type { HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export const stateToIsoDateString = (entityState: HassEntityBase) =>
`${entityState}T00:00:00`;
export const setDateValue = (
callService: HomeAssistantApi["callService"],
hass: HomeAssistant,
entityId: string,
date: string | undefined = undefined
) => {
const param = { entity_id: entityId, date };
callService("date", "set_value", param);
hass.callService("date", "set_value", param);
};
+3 -3
View File
@@ -1,11 +1,11 @@
import type { HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export const setDateTimeValue = (
callService: HomeAssistantApi["callService"],
hass: HomeAssistant,
entityId: string,
datetime: Date
) => {
callService("datetime", "set_value", {
hass.callService("datetime", "set_value", {
entity_id: entityId,
datetime: datetime.toISOString(),
});
+1 -1
View File
@@ -277,7 +277,7 @@ export const getExtendedEntityRegistryEntries = (
});
export const updateEntityRegistryEntry = (
hass: Pick<HomeAssistant, "callWS">,
hass: HomeAssistant,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<UpdateEntityRegistryEntryResult> =>
+3 -4
View File
@@ -7,12 +7,11 @@ interface EntitySource {
export type EntitySources = Record<string, EntitySource>;
const fetchEntitySources = (
hass: Pick<HomeAssistant, "callWS">
): Promise<EntitySources> => hass.callWS({ type: "entity/source" });
const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
hass.callWS({ type: "entity/source" });
export const fetchEntitySourcesWithCache = (
hass: Pick<HomeAssistant, "callWS" | "states">
hass: HomeAssistant
): Promise<EntitySources> =>
timeCachePromiseFunc(
"_entitySources",
+1 -1
View File
@@ -12,7 +12,7 @@ import type {
import { stateActive } from "../common/entity/state_active";
import type { HomeAssistant } from "../types";
export enum FanEntityFeature {
export const enum FanEntityFeature {
SET_SPEED = 1,
OSCILLATE = 2,
DIRECTION = 4,
+2 -6
View File
@@ -8,13 +8,9 @@ export const uploadFile = async (hass: HomeAssistant, file: File) => {
body: fd,
});
if (resp.status === 413) {
throw new Error(
hass.localize("ui.common.upload_file_too_large", {
name: file.name,
})
);
throw new Error(`Uploaded file is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error(hass.localize("ui.common.unknown_error"));
throw new Error("Unknown error");
}
const data = await resp.json();
return data.file_id;
+1 -1
View File
@@ -20,7 +20,7 @@ export type HumidifierEntity = HassEntityBase & {
};
};
export enum HumidifierEntityFeature {
export const enum HumidifierEntityFeature {
MODES = 1,
}
+6 -8
View File
@@ -548,9 +548,7 @@ const getEntityIcon = async (
};
export const attributeIcon = async (
hassConfig: HomeAssistant["config"],
hassConnection: HomeAssistant["connection"],
entities: HomeAssistant["entities"],
hass: HomeAssistant,
state: HassEntity,
attribute: string,
attributeValue?: string
@@ -558,7 +556,7 @@ export const attributeIcon = async (
let icon: string | undefined;
const domain = computeStateDomain(state);
const deviceClass = state.attributes.device_class;
const entity = entities[state.entity_id] as
const entity = hass.entities?.[state.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const platform = entity?.platform;
@@ -569,8 +567,8 @@ export const attributeIcon = async (
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(
hassConfig,
hassConnection,
hass.config,
hass.connection,
platform
);
if (platformIcons) {
@@ -582,8 +580,8 @@ export const attributeIcon = async (
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(
hassConnection,
hassConfig,
hass.connection,
hass.config,
domain
);
if (entityComponentIcons) {
+2 -6
View File
@@ -57,13 +57,9 @@ export const createImage = async (
body: fd,
});
if (resp.status === 413) {
throw new Error(
hass.localize("ui.common.upload_image_too_large", {
name: file.name,
})
);
throw new Error(`Uploaded image is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error(hass.localize("ui.common.unknown_error"));
throw new Error("Unknown error");
}
return resp.json();
};
+3 -3
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface InputDateTime {
id: string;
@@ -32,13 +32,13 @@ export const stateToIsoDateString = (entityState: HassEntity) =>
)}`;
export const setInputDateTimeValue = (
callService: HomeAssistantApi["callService"],
hass: HomeAssistant,
entityId: string,
time: string | undefined = undefined,
date: string | undefined = undefined
) => {
const param = { entity_id: entityId, time, date };
callService("input_datetime", "set_datetime", param);
hass.callService("input_datetime", "set_datetime", param);
};
export const fetchInputDateTime = (hass: HomeAssistant) =>
+1 -1
View File
@@ -11,7 +11,7 @@ export type LawnMowerEntityState =
| "docked"
| "error";
export enum LawnMowerEntityFeature {
export const enum LawnMowerEntityFeature {
START_MOWING = 1,
PAUSE = 2,
DOCK = 4,
+1 -1
View File
@@ -4,7 +4,7 @@ import type {
} from "home-assistant-js-websocket";
import { temperature2rgb } from "../common/color/convert-light-color";
export enum LightEntityFeature {
export const enum LightEntityFeature {
EFFECT = 4,
FLASH = 8,
TRANSITION = 32,
+1 -1
View File
@@ -7,7 +7,7 @@ import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export enum LockEntityFeature {
export const enum LockEntityFeature {
OPEN = 1,
}
+195 -78
View File
@@ -1,9 +1,15 @@
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../common/const";
import {
BINARY_STATE_OFF,
BINARY_STATE_ON,
DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { autoCaseNoun } from "../common/translations/auto_case_noun";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
import { isNumericEntity } from "./history";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
@@ -23,7 +29,7 @@ export interface LogbookEntry {
message?: string;
entity_id?: string;
icon?: string;
source?: string; // The trigger source (English phrase, parsed for the cause)
source?: string; // The trigger source
domain?: string;
state?: string; // The state of the entity
// Context data
@@ -44,27 +50,23 @@ export interface LogbookEntry {
// Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers
//
// Keys are the bare translation keys under `ui.components.logbook`.
//
type TriggerPhraseKey =
| "numeric_state_of"
| "state_of"
| "event"
| "time_pattern"
| "time"
| "homeassistant_stopping"
| "homeassistant_starting";
type TriggerPhraseKeys =
| "triggered_by_numeric_state_of"
| "triggered_by_state_of"
| "triggered_by_event"
| "triggered_by_time"
| "triggered_by_time_pattern"
| "triggered_by_homeassistant_stopping"
| "triggered_by_homeassistant_starting";
// Order matters: "time pattern" must be tested before "time" because the
// source phrase is matched with `startsWith`.
const triggerPhrases: Record<TriggerPhraseKey, string> = {
numeric_state_of: "numeric state of", // number state trigger
state_of: "state of", // state trigger
event: "event", // event trigger
time_pattern: "time pattern", // time trigger
time: "time", // time trigger
homeassistant_stopping: "Home Assistant stopping", // stop event
homeassistant_starting: "Home Assistant starting", // start event
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
triggered_by_numeric_state_of: "numeric state of", // number state trigger
triggered_by_state_of: "state of", // state trigger
triggered_by_event: "event", // event trigger
triggered_by_time_pattern: "time pattern", // time trigger
triggered_by_time: "time", // time trigger
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
};
export const getLogbookDataForContext = async (
@@ -156,100 +158,215 @@ export const createHistoricState = (
state: state,
attributes: {
// Rebuild the historical state by copying static attributes only
device_class: currentStateObj.attributes.device_class,
unit_of_measurement: currentStateObj.attributes.unit_of_measurement,
state_class: currentStateObj.attributes.state_class,
options: currentStateObj.attributes.options,
source_type: currentStateObj.attributes.source_type,
has_date: currentStateObj.attributes.has_date,
has_time: currentStateObj.attributes.has_time,
device_class: currentStateObj?.attributes.device_class,
source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time,
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
// as they would present a false state in the log (played media right now vs actual historic data).
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj.attributes.entity_picture_local,
: currentStateObj?.attributes.entity_picture_local,
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
computeDomain(currentStateObj.entity_id)
)
? undefined
: currentStateObj.attributes.entity_picture,
: currentStateObj?.attributes.entity_picture,
},
}) as unknown as HassEntity;
// Localize a backend trigger `source` phrase (e.g. "state of sensor.x") by
// translating the leading phrase while keeping the entity id. The automation
// trace timeline frames it with its own "triggered by" wording, so we only
// translate the bare description here.
export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string
) => {
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
const phrase = triggerPhrases[key];
for (const triggerPhraseKey of Object.keys(
triggerPhrases
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
if (source.startsWith(phrase)) {
return source.replace(phrase, localize(`ui.components.logbook.${key}`));
return source.replace(
phrase,
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
);
}
}
return source;
};
export type TriggerPlatform =
| "state"
| "numeric_state"
// Mapping from a phrase key to the bare-phrase translation key (without the
// "triggered by" prefix), used by localizeTriggerDescription below.
const triggerDescriptionKeys: Record<
TriggerPhraseKeys,
| "numeric_state_of"
| "state_of"
| "event"
| "time"
| "time_pattern"
| "event"
| "homeassistant";
// Maps the English `triggerPhrases` to automation trigger platforms, so the
// feed can reuse the editor's trigger-type labels instead of dedicated strings.
const triggerPlatform: Record<TriggerPhraseKey, TriggerPlatform> = {
numeric_state_of: "numeric_state",
state_of: "state",
event: "event",
time_pattern: "time_pattern",
time: "time",
homeassistant_stopping: "homeassistant",
homeassistant_starting: "homeassistant",
| "homeassistant_stopping"
| "homeassistant_starting"
> = {
triggered_by_numeric_state_of: "numeric_state_of",
triggered_by_state_of: "state_of",
triggered_by_event: "event",
triggered_by_time_pattern: "time_pattern",
triggered_by_time: "time",
triggered_by_homeassistant_stopping: "homeassistant_stopping",
triggered_by_homeassistant_starting: "homeassistant_starting",
};
export interface ParsedTriggerSource {
platform?: TriggerPlatform;
entityId?: string;
}
// Best-effort parse of the backend's English trigger `source` (e.g. "numeric
// state of sensor.x", "time pattern") into a platform + triggering entity.
// Temporary bridge until the backend sends the trigger structurally.
export const parseTriggerSource = (source: string): ParsedTriggerSource => {
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
const phrase = triggerPhrases[key];
if (!source.startsWith(phrase)) {
continue;
// Like localizeTriggerSource, but returns just the bare localized trigger
// description (without the "triggered by" prefix). Used where the surrounding
// template already supplies its own "triggered by" wording.
export const localizeTriggerDescription = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhraseKey of Object.keys(
triggerPhrases
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
if (source.startsWith(phrase)) {
const bareKey = triggerDescriptionKeys[triggerPhraseKey];
return source.replace(
phrase,
`${localize(`ui.components.logbook.${bareKey}`)}`
);
}
const rest = source.slice(phrase.length).trim();
const entityId = /^[a-z_]+\.[a-z0-9_]+$/.test(rest) ? rest : undefined;
return { platform: triggerPlatform[key], entityId };
}
return {};
return source;
};
export const localizeStateMessage = (
hass: HomeAssistant,
localize: LocalizeFunc,
state: string,
stateObj: HassEntity,
domain: string
): string => {
// Events expose a timestamp as their state, which has no meaningful display
// value, so keep a dedicated phrase.
if (domain === "event") {
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
switch (domain) {
case "device_tracker":
case "person":
if (state === "not_home") {
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
}
if (state === "home") {
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
}
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_state`, { state });
case "sun":
return state === "above_horizon"
? localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
: localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
case "binary_sensor": {
const isOn = state === BINARY_STATE_ON;
const isOff = state === BINARY_STATE_OFF;
const device_class = stateObj.attributes.device_class;
if (device_class && (isOn || isOff)) {
return (
localize(
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_classes" : "cleared_device_classes"}.${device_class}`,
{
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
) || device_class,
hass.language
),
}
) ||
// If there's no key for a specific device class, fallback to generic string
localize(
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_class" : "cleared_device_class"}`,
{
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
) || device_class,
hass.language
),
}
)
);
}
break;
}
case "cover":
switch (state) {
case "open":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
case "event": {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
// TODO: This is not working yet, as we don't get historic attribute values
const event_type = hass
.formatEntityAttributeValue(stateObj, "event_type")
?.toString();
if (!event_type) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
}
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event`, {
event_type: autoCaseNoun(event_type, hass.language),
});
}
case "lock":
switch (state) {
case "unlocked":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
case "locking":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
case "unlocking":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
case "opening":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "open":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opened`);
case "locked":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
case "jammed":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_jammed`);
}
break;
}
// Every other domain reuses the backend state translation, so the logbook
// speaks the same vocabulary as the rest of the UI.
return hass.formatEntityState(stateObj, state);
if (state === BINARY_STATE_ON) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`);
}
if (state === BINARY_STATE_OFF) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
}
if (state === UNKNOWN) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unknown`);
}
if (state === UNAVAILABLE) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
}
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, {
state: stateObj ? hass.formatEntityState(stateObj, state) : state,
});
};
export const filterLogbookCompatibleEntities = (entity) => {
+1 -1
View File
@@ -82,7 +82,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
| "buffering";
}
export enum MediaPlayerEntityFeature {
export const enum MediaPlayerEntityFeature {
PAUSE = 1,
SEEK = 2,
VOLUME_SET = 4,
+2 -6
View File
@@ -54,13 +54,9 @@ export const uploadLocalMedia = async (
}
);
if (resp.status === 413) {
throw new Error(
hass.localize("ui.common.upload_file_too_large", {
name: file.name,
})
);
throw new Error(`Uploaded file is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error(hass.localize("ui.common.unknown_error"));
throw new Error("Unknown error");
}
return resp.json();
};
+9 -30
View File
@@ -1,7 +1,6 @@
import type {
HassEntityAttributeBase,
HassEntityBase,
HassServices,
HassServiceTarget,
} from "home-assistant-js-websocket";
import type { Describe } from "superstruct";
@@ -105,9 +104,6 @@ export interface Field {
selector?: any;
}
const getScriptFields = (services: HassServices, entityId: string) =>
services.script[computeObjectId(entityId)]?.fields;
interface BaseAction {
alias?: string;
note?: string;
@@ -395,41 +391,31 @@ export const getActionType = (action: Action): ActionType => {
export const isAction = (value: unknown): value is Action =>
getActionType(value as Action) !== "unknown";
export const hasScriptFieldsForServices = (
services: HassServices,
entityId: string
): boolean => {
const fields = getScriptFields(services, entityId);
return fields !== undefined && Object.keys(fields).length > 0;
};
export const hasScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => hasScriptFieldsForServices(hass.services, entityId);
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return fields !== undefined && Object.keys(fields).length > 0;
};
export const hasRequiredScriptFieldsForServices = (
services: HassServices,
export const hasRequiredScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => {
const fields = getScriptFields(services, entityId);
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return (
fields !== undefined &&
Object.values(fields).some((field) => field.required)
);
};
export const hasRequiredScriptFields = (
export const requiredScriptFieldsFilled = (
hass: HomeAssistant,
entityId: string
): boolean => hasRequiredScriptFieldsForServices(hass.services, entityId);
export const requiredScriptFieldsFilledForServices = (
services: HassServices,
entityId: string,
data?: Record<string, any>
): boolean => {
const fields = getScriptFields(services, entityId);
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
if (fields === undefined || Object.keys(fields).length === 0) {
return true;
}
@@ -444,13 +430,6 @@ export const requiredScriptFieldsFilledForServices = (
});
};
export const requiredScriptFieldsFilled = (
hass: HomeAssistant,
entityId: string,
data?: Record<string, any>
): boolean =>
requiredScriptFieldsFilledForServices(hass.services, entityId, data);
export const migrateAutomationAction = (
action: Action | Action[]
): Action | Action[] => {
-5
View File
@@ -82,7 +82,6 @@ export type Selector =
| UiActionSelector
| UiColorSelector
| UiStateContentSelector
| UiTimeFormatSelector
| BackupLocationSelector;
export interface ActionSelector {
@@ -602,10 +601,6 @@ export interface UiStateContentSelector {
} | null;
}
export interface UiTimeFormatSelector {
ui_time_format: {} | null;
}
export interface EntityNameSelector {
entity_name: {
entity_id?: string;
+1 -3
View File
@@ -6,9 +6,7 @@ export interface SupervisorUpdateConfig {
core_backup_before_update: boolean;
}
export const getSupervisorUpdateConfig = async (
hass: Pick<HomeAssistant, "callWS">
) =>
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
hass.callWS<SupervisorUpdateConfig>({
type: "hassio/update/config/info",
});
+3 -3
View File
@@ -1,10 +1,10 @@
import type { HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export const setTimeValue = (
callService: HomeAssistantApi["callService"],
hass: HomeAssistant,
entityId: string,
time: string | undefined = undefined
) => {
const param = { entity_id: entityId, time: time };
callService("time", "set_value", param);
hass.callService("time", "set_value", param);
};
-10
View File
@@ -3,10 +3,8 @@ import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { createDurationData } from "../common/datetime/create_duration_data";
import durationToSeconds from "../common/datetime/duration_to_seconds";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import type { HaDurationData } from "../components/ha-duration-input";
import type { HomeAssistant } from "../types";
export type TimerEntity = HassEntityBase & {
@@ -102,11 +100,3 @@ export const computeDisplayTimer = (
return display;
};
// Prefill for the duration input: always the configured duration, independent
// of the live countdown. The field is meant to be edited, not to mirror the
// remaining time.
export const timerDurationData = (
stateObj: HassEntity
): HaDurationData | undefined =>
createDurationData(stateObj.attributes.duration);
+1 -1
View File
@@ -31,7 +31,7 @@ export interface TodoItem {
completed?: string | null;
}
export enum TodoListEntityFeature {
export const enum TodoListEntityFeature {
CREATE_TODO_ITEM = 1,
DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4,
+1 -4
View File
@@ -77,10 +77,7 @@ export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress;
export const updateReleaseNotes = (
hass: Pick<HomeAssistant, "callWS">,
entityId: string
) =>
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({
type: "update/release_notes",
entity_id: entityId,
+1 -1
View File
@@ -15,7 +15,7 @@ export type VacuumEntityState =
| "returning"
| "error";
export enum VacuumEntityFeature {
export const enum VacuumEntityFeature {
TURN_ON = 1,
TURN_OFF = 2,
PAUSE = 4,
+4 -4
View File
@@ -4,10 +4,10 @@ import type {
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistantFormatters } from "../types";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export enum ValveEntityFeature {
export const enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
@@ -78,7 +78,7 @@ export interface ValveEntity extends HassEntityBase {
export function computeValvePositionStateDisplay(
stateObj: ValveEntity,
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
@@ -88,7 +88,7 @@ export function computeValvePositionStateDisplay(
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? formatEntityAttributeValue(
? hass.formatEntityAttributeValue(
stateObj,
"current_position",
Math.round(currentPosition)
+1 -1
View File
@@ -3,7 +3,7 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
export enum WaterHeaterEntityFeature {
export const enum WaterHeaterEntityFeature {
TARGET_TEMPERATURE = 1,
OPERATION_MODE = 2,
AWAY_MODE = 4,
+1 -1
View File
@@ -41,7 +41,7 @@ import { round } from "../common/number/round";
import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types";
export enum WeatherEntityFeature {
export const enum WeatherEntityFeature {
FORECAST_DAILY = 1,
FORECAST_HOURLY = 2,
FORECAST_TWICE_DAILY = 4,
@@ -1,16 +1,11 @@
import { consume } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
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 { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { transform } from "../../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-control-button";
import { apiContext, configContext } from "../../../../data/context";
import type { CoverEntity } from "../../../../data/cover";
import {
DEFAULT_COVER_FAVORITE_POSITIONS,
@@ -25,11 +20,7 @@ import type {
ExtEntityRegistryEntry,
} from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
} from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import {
showConfirmationDialog,
showPromptDialog,
@@ -55,20 +46,7 @@ const favoriteKindFromEvent = (ev: Event): FavoriteKind =>
@customElement("ha-more-info-cover-favorite-positions")
export class HaMoreInfoCoverFavoritePositions extends LitElement {
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
transformer: ({ user }) => user,
})
private _user!: HomeAssistant["user"];
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity;
@@ -107,7 +85,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
key: FavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this._localize(
return this.hass.localize(
`ui.dialogs.more_info_control.cover.${kind === "position" ? "favorite_position" : "favorite_tilt_position"}.${key}`,
values
);
@@ -146,7 +124,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
const result = await updateEntityRegistryEntry(
this._api,
this.hass,
this.entry.entity_id,
{
options_domain: "cover",
@@ -191,14 +169,14 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
if (kind === "position") {
this._api.callService("cover", "set_cover_position", {
this.hass.callService("cover", "set_cover_position", {
entity_id: this.stateObj.entity_id,
position: favorite,
});
return;
}
this._api.callService("cover", "set_cover_tilt_position", {
this.hass.callService("cover", "set_cover_tilt_position", {
entity_id: this.stateObj.entity_id,
tilt_position: favorite,
});
@@ -213,7 +191,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
kind,
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this._localize(
inputLabel: this.hass.localize(
kind === "position"
? "ui.card.cover.position"
: "ui.card.cover.tilt_position"
@@ -333,7 +311,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
const { action, index } = ev.detail;
if (action === "hold" && this._user?.is_admin) {
if (action === "hold" && this.hass.user?.is_admin) {
fireEvent(this, "toggle-edit-mode", true);
return;
}
@@ -398,10 +376,10 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
.deleteLabel=${this._deleteLabel(kind)}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this._user?.is_admin)}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.showDone=${showDone}
.addLabel=${this._localizeFavorite(kind, "add")}
.doneLabel=${this._localize(
.doneLabel=${this.hass.localize(
"ui.dialogs.more_info_control.exit_edit_mode"
)}
@favorite-item-action=${this._handleFavoriteAction}
@@ -437,7 +415,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
${supportsPosition
? this._renderKindSection(
"position",
this._localize("ui.card.cover.position"),
this.hass.localize("ui.card.cover.position"),
this._favoritePositions,
showDoneOnPosition,
showLabels
@@ -446,7 +424,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
${supportsTiltPosition
? this._renderKindSection(
"tilt",
this._localize("ui.card.cover.tilt_position"),
this.hass.localize("ui.card.cover.tilt_position"),
this._favoriteTiltPositions,
true,
showLabels
@@ -1,18 +1,18 @@
import { consume } from "@lit/context";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-absolute-time";
import "../../../components/ha-relative-time";
import type { HomeAssistantFormatters } from "../../../types";
import { formattersContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { LightEntity } from "../../../data/light";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display";
import type { HomeAssistant } from "../../../types";
@customElement("ha-more-info-state-header")
export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@property({ attribute: false }) public stateOverride?: string;
@@ -21,10 +21,6 @@ export class HaMoreInfoStateHeader extends LitElement {
@state() private _absoluteTime = false;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
private _localizeState(): TemplateResult | string {
if (
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
@@ -33,6 +29,7 @@ export class HaMoreInfoStateHeader extends LitElement {
) {
return html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(this.stateObj.state)}
format="relative"
capitalize
@@ -40,7 +37,7 @@ export class HaMoreInfoStateHeader extends LitElement {
`;
}
return this._formatters?.formatEntityState(this.stateObj) ?? "";
return this.hass.formatEntityState(this.stateObj);
}
private _toggleAbsolute() {
@@ -1,16 +1,11 @@
import { consume } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
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 { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { transform } from "../../../../common/decorators/transform";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-control-button";
import { apiContext, configContext } from "../../../../data/context";
import { UNAVAILABLE } from "../../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
import type {
@@ -18,11 +13,7 @@ import type {
ValveEntityOptions,
} from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
} from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { ValveEntity } from "../../../../data/valve";
import { DEFAULT_VALVE_FAVORITE_POSITIONS } from "../../../../data/valve";
import { normalizeFavoritePositions } from "../../../../data/favorite_positions";
@@ -46,20 +37,7 @@ type FavoriteLocalizeKey =
@customElement("ha-more-info-valve-favorite-positions")
export class HaMoreInfoValveFavoritePositions extends LitElement {
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
transformer: ({ user }) => user,
})
private _user!: HomeAssistant["user"];
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
@@ -86,7 +64,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
key: FavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this._localize(
return this.hass.localize(
`ui.dialogs.more_info_control.valve.favorite_position.${key}`,
values
);
@@ -110,7 +88,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
currentOptions.favorite_positions = this._favoritePositions;
const result = await updateEntityRegistryEntry(
this._api,
this.hass,
this.entry.entity_id,
{
options_domain: "valve",
@@ -144,7 +122,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
return;
}
this._api.callService("valve", "set_valve_position", {
this.hass.callService("valve", "set_valve_position", {
entity_id: this.stateObj.entity_id,
position: favorite,
});
@@ -157,7 +135,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
title: this._localizeFavorite(
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this._localize("ui.card.valve.position"),
inputLabel: this.hass.localize("ui.card.valve.position"),
inputType: "number",
inputMin: "0",
inputMax: "100",
@@ -264,7 +242,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
const { action, index } = ev.detail;
if (action === "hold" && this._user?.is_admin) {
if (action === "hold" && this.hass.user?.is_admin) {
fireEvent(this, "toggle-edit-mode", true);
return;
}
@@ -318,10 +296,10 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
.deleteLabel=${this._deleteLabel}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this._user?.is_admin)}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.showDone=${true}
.addLabel=${this._localizeFavorite("add")}
.doneLabel=${this._localize(
.doneLabel=${this.hass.localize(
"ui.dialogs.more_info_control.exit_edit_mode"
)}
@favorite-item-action=${this._handleFavoriteAction}
@@ -1,37 +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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import "../../../components/ha-relative-time";
import { apiContext } from "../../../data/context";
import { triggerAutomationActions } from "../../../data/automation";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistantApi } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-automation")
class MoreInfoAutomation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
return html`
<hr />
<div class="flex">
<div>${this._localize("ui.card.automation.last_triggered")}:</div>
<div>${this.hass.localize("ui.card.automation.last_triggered")}:</div>
<ha-relative-time
.datetime=${this.stateObj.attributes.last_triggered}
capitalize
@@ -45,14 +35,14 @@ class MoreInfoAutomation extends LitElement {
@click=${this._runActions}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
${this._localize("ui.card.automation.trigger")}
${this.hass.localize("ui.card.automation.trigger")}
</ha-button>
</div>
`;
}
private _runActions() {
triggerAutomationActions(this._api, this.stateObj!.entity_id);
triggerAutomationActions(this.hass, this.stateObj!.entity_id);
}
static styles = css`
@@ -1,5 +1,3 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import {
mdiArrowOscillating,
mdiFan,
@@ -10,9 +8,7 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
@@ -26,10 +22,10 @@ import {
climateHvacModeIcon,
compareClimateHvacModes,
} from "../../../data/climate";
import { apiContext, formattersContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import "../../../state-control/climate/ha-state-control-climate-humidity";
import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -38,24 +34,15 @@ type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -63,6 +50,7 @@ class MoreInfoClimate extends LitElement {
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
@@ -70,6 +58,7 @@ class MoreInfoClimate extends LitElement {
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
@@ -77,6 +66,7 @@ class MoreInfoClimate extends LitElement {
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
@@ -130,13 +120,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"current_temperature"
)}
</p>
<p class="value">
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}
@@ -148,13 +138,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -167,6 +157,7 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "temperature"
? html`
<ha-state-control-climate-temperature
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-temperature>
`
@@ -174,6 +165,7 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "humidity"
? html`
<ha-state-control-climate-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-humidity>
`
@@ -184,7 +176,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "temperature"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.more_info_control.climate.temperature"
)}
.control=${"temperature"}
@@ -195,7 +187,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "humidity"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.more_info_control.climate.humidity"
)}
.control=${"humidity"}
@@ -209,7 +201,8 @@ class MoreInfoClimate extends LitElement {
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this._localize("ui.card.climate.mode")}
.hass=${this.hass}
.label=${this.hass.localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
.options=${stateObj.attributes.hvac_modes
@@ -218,7 +211,7 @@ class MoreInfoClimate extends LitElement {
.map((mode) => ({
value: mode,
iconPath: climateHvacModeIcon(mode),
label: this._formatters.formatEntityState(stateObj, mode),
label: this.hass.formatEntityState(stateObj, mode),
}))}
@wa-select=${this._handleOperationModeChanged}
>
@@ -230,7 +223,8 @@ class MoreInfoClimate extends LitElement {
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this._formatters.formatEntityAttributeName(
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
@@ -239,7 +233,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handlePresetmodeChanged}
.options=${stateObj.attributes.preset_modes.map((mode) => ({
value: mode,
label: this._formatters.formatEntityAttributeValue(
label: this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
@@ -254,7 +248,8 @@ class MoreInfoClimate extends LitElement {
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.label=${this._formatters.formatEntityAttributeName(
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
@@ -263,7 +258,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleFanModeChanged}
.options=${stateObj.attributes.fan_modes.map((mode) => ({
value: mode,
label: this._formatters.formatEntityAttributeValue(
label: this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
@@ -278,7 +273,8 @@ class MoreInfoClimate extends LitElement {
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.label=${this._formatters.formatEntityAttributeName(
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
@@ -287,7 +283,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleSwingmodeChanged}
.options=${stateObj.attributes.swing_modes.map((mode) => ({
value: mode,
label: this._formatters.formatEntityAttributeValue(
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
@@ -306,7 +302,8 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.swing_horizontal_modes
? html`
<ha-control-select-menu
.label=${this._formatters.formatEntityAttributeName(
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_horizontal_mode"
)}
@@ -316,7 +313,7 @@ class MoreInfoClimate extends LitElement {
.options=${stateObj.attributes.swing_horizontal_modes.map(
(mode) => ({
value: mode,
label: this._formatters.formatEntityAttributeValue(
label: this.hass.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
@@ -406,7 +403,7 @@ class MoreInfoClimate extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this._api.callService("climate", service, data);
await this.hass.callService("climate", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -1,4 +1,3 @@
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";
@@ -6,20 +5,17 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import "../../../components/input/ha-input";
import { apiContext } from "../../../data/context";
import type { HomeAssistantApi } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-configurator")
export class MoreInfoConfigurator extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _isConfiguring = false;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
private _fieldInput: Record<string, unknown> = {};
private _fieldInput = {};
protected render() {
if (this.stateObj?.state !== "configure") {
@@ -75,7 +71,7 @@ export class MoreInfoConfigurator extends LitElement {
this._isConfiguring = true;
this._api.callService("configurator", "configure", data).then(
this.hass.callService("configurator", "configure", data).then(
() => {
this._isConfiguring = false;
},
@@ -1,12 +1,8 @@
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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { apiContext } from "../../../data/context";
import type { HomeAssistantApi } from "../../../types";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-assist-chat";
import "../../../components/ha-spinner";
import "../../../components/ha-alert";
@@ -15,20 +11,14 @@ import { getAssistPipeline } from "../../../data/assist_pipeline";
@customElement("more-info-conversation")
class MoreInfoConversation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() public _pipeline?: AssistPipeline;
@state() private _errorLoadAssist?: "not_found" | "unknown";
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
@@ -50,7 +40,7 @@ class MoreInfoConversation extends LitElement {
this._errorLoadAssist = undefined;
const pipelineId = this.stateObj!.entity_id;
try {
const pipeline = await getAssistPipeline(this._api, pipelineId);
const pipeline = await getAssistPipeline(this.hass, pipelineId);
// Verify the pipeline is still the same.
if (this.stateObj && pipelineId === this.stateObj.entity_id) {
this._pipeline = pipeline;
@@ -71,20 +61,21 @@ class MoreInfoConversation extends LitElement {
}
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
return html`
${this._errorLoadAssist
? html`<ha-alert alert-type="error">
${this._localize(
${this.hass.localize(
`ui.dialogs.voice_command.${this._errorLoadAssist}_error_load_assist`
)}
</ha-alert>`
: this._pipeline
? html`
<ha-assist-chat
.hass=${this.hass}
.pipeline=${this._pipeline}
disable-speech
></ha-assist-chat>
@@ -1,28 +1,18 @@
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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistantApi } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-counter")
class MoreInfoCounter extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -38,7 +28,7 @@ class MoreInfoCounter extends LitElement {
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
>
${this._localize("ui.card.counter.actions.increment")}
${this.hass!.localize("ui.card.counter.actions.increment")}
</ha-button>
<ha-button
appearance="plain"
@@ -48,7 +38,7 @@ class MoreInfoCounter extends LitElement {
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
>
${this._localize("ui.card.counter.actions.decrement")}
${this.hass!.localize("ui.card.counter.actions.decrement")}
</ha-button>
<ha-button
appearance="plain"
@@ -57,7 +47,7 @@ class MoreInfoCounter extends LitElement {
@click=${this._handleActionClick}
.disabled=${disabled}
>
${this._localize("ui.card.counter.actions.reset")}
${this.hass!.localize("ui.card.counter.actions.reset")}
</ha-button>
</div>
`;
@@ -65,7 +55,7 @@ class MoreInfoCounter extends LitElement {
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
this._api.callService("counter", action, {
this.hass.callService("counter", action, {
entity_id: this.stateObj!.entity_id,
});
}
@@ -1,14 +1,10 @@
import { consume } from "@lit/context";
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import { formattersContext } from "../../../data/context";
import {
shouldShowFavoriteOptions,
type ExtEntityRegistryEntry,
@@ -25,7 +21,7 @@ import "../../../state-control/cover/ha-state-control-cover-buttons";
import "../../../state-control/cover/ha-state-control-cover-position";
import "../../../state-control/cover/ha-state-control-cover-tilt-position";
import "../../../state-control/cover/ha-state-control-cover-toggle";
import type { HomeAssistantFormatters } from "../../../types";
import type { HomeAssistant } from "../../../types";
import "../components/covers/ha-more-info-cover-favorite-positions";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@@ -34,13 +30,7 @@ type Mode = "position" | "button";
@customElement("more-info-cover")
class MoreInfoCover extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: CoverEntity;
@@ -68,11 +58,11 @@ class MoreInfoCover extends LitElement {
}
private get _stateOverride() {
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const positionStateDisplay = computeCoverPositionStateDisplay(
this.stateObj!,
this._formatters.formatEntityAttributeValue
this.hass
);
if (positionStateDisplay) {
@@ -82,7 +72,7 @@ class MoreInfoCover extends LitElement {
}
protected render() {
if (!this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -123,6 +113,7 @@ class MoreInfoCover extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
@@ -135,6 +126,7 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-position>
`
: nothing}
@@ -142,6 +134,7 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-tilt-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-tilt-position>
`
: nothing}
@@ -155,12 +148,14 @@ class MoreInfoCover extends LitElement {
? html`
<ha-state-control-cover-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-toggle>
`
: supportsOpenClose || supportsTilt
? html`
<ha-state-control-cover-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-cover-buttons>
`
: nothing}
@@ -174,7 +169,7 @@ class MoreInfoCover extends LitElement {
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this._localize(
.label=${this.hass.localize(
`ui.dialogs.more_info_control.cover.switch_mode.position`
)}
.selected=${this._mode === "position"}
@@ -183,7 +178,7 @@ class MoreInfoCover extends LitElement {
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this._localize(
.label=${this.hass.localize(
`ui.dialogs.more_info_control.cover.switch_mode.button`
)}
.selected=${this._mode === "button"}
@@ -200,6 +195,7 @@ class MoreInfoCover extends LitElement {
showFavoriteControls
? html`
<ha-more-info-cover-favorite-positions
.hass=${this.hass}
.stateObj=${this.stateObj}
.entry=${this.entry}
.editMode=${this.editMode}
@@ -1,35 +1,18 @@
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 { transform } from "../../../common/decorators/transform";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { setDateValue } from "../../../data/date";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-date")
class MoreInfoDate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
return nothing;
@@ -37,7 +20,7 @@ class MoreInfoDate extends LitElement {
return html`
<ha-date-input
.locale=${this._locale}
.locale=${this.hass.locale}
.value=${this.stateObj.state === UNKNOWN
? undefined
: this.stateObj.state}
@@ -49,11 +32,7 @@ class MoreInfoDate extends LitElement {
private _dateChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
setDateValue(
this._api.callService,
this.stateObj!.entity_id,
ev.detail.value
);
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
}
@@ -1,36 +1,19 @@
import { consume } from "@lit/context";
import { format } from "date-fns";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { setDateTimeValue } from "../../../data/datetime";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-datetime")
class MoreInfoDatetime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
return nothing;
@@ -44,14 +27,14 @@ class MoreInfoDatetime extends LitElement {
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
return html`<ha-date-input
.locale=${this._locale}
.locale=${this.hass.locale}
.value=${date}
@value-changed=${this._dateChanged}
>
</ha-date-input>
<ha-time-input
.value=${time}
.locale=${this._locale}
.locale=${this.hass.locale}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>`;
@@ -67,11 +50,7 @@ class MoreInfoDatetime extends LitElement {
const newTime = ev.detail.value.split(":").map(Number);
dateObj.setHours(newTime[0], newTime[1], newTime[2]);
setDateTimeValue(
this._api.callService,
this.stateObj!.entity_id,
dateObj
);
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
}
}
@@ -81,11 +60,7 @@ class MoreInfoDatetime extends LitElement {
const newDate = ev.detail.value.split("-").map(Number);
dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]);
setDateTimeValue(
this._api.callService,
this.stateObj!.entity_id,
dateObj
);
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
}
}
@@ -42,6 +42,7 @@ class MoreInfoFan extends LitElement {
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -49,6 +50,7 @@ class MoreInfoFan extends LitElement {
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
@@ -239,6 +241,7 @@ class MoreInfoFan extends LitElement {
>
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
@@ -1,44 +1,31 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { apiContext, formattersContext } from "../../../data/context";
import type { HumidifierEntity } from "../../../data/humidifier";
import { HumidifierEntityFeature } from "../../../data/humidifier";
import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
@@ -56,6 +43,7 @@ class MoreInfoHumidifier extends LitElement {
return nothing;
}
const hass = this.hass;
const stateObj = this.stateObj;
const supportModes = supportsFeature(
@@ -69,13 +57,13 @@ class MoreInfoHumidifier extends LitElement {
? html`
<div>
<p class="label">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -87,20 +75,22 @@ class MoreInfoHumidifier extends LitElement {
<div class="controls">
<ha-state-control-humidifier-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-humidifier-humidity>
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this._localize("ui.card.humidifier.state")}
.hass=${hass}
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleStateChanged}
.options=${["off", "on"].map((fanState) => ({
value: fanState,
label: this.stateObj
? this._formatters.formatEntityState(this.stateObj, fanState)
? this.hass.formatEntityState(this.stateObj, fanState)
: fanState,
}))}
>
@@ -110,14 +100,15 @@ class MoreInfoHumidifier extends LitElement {
${supportModes
? html`
<ha-control-select-menu
.label=${this._localize("ui.card.humidifier.mode")}
.hass=${hass}
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleModeChanged}
.options=${stateObj.attributes.available_modes?.map((mode) => ({
value: mode,
label: stateObj
? this._formatters.formatEntityAttributeValue(
? this.hass.formatEntityAttributeValue(
stateObj,
"mode",
mode
@@ -173,7 +164,7 @@ class MoreInfoHumidifier extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this._api.callService("humidifier", service, data);
await this.hass.callService("humidifier", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -2,31 +2,32 @@ import { mdiPower, mdiPowerOff } 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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { customElement, property } from "lit/decorators";
import "../../../state-control/ha-state-control-toggle";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-input_boolean")
class MoreInfoInputBoolean extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls">
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiPower}
.iconPathOff=${mdiPowerOff}
></ha-state-control-toggle>
@@ -1,38 +1,21 @@
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 { transform } from "../../../common/decorators/transform";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-input_datetime")
class MoreInfoInputDatetime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -42,7 +25,7 @@ class MoreInfoInputDatetime extends LitElement {
${this.stateObj.attributes.has_date
? html`
<ha-date-input
.locale=${this._locale}
.locale=${this.hass.locale}
.value=${stateToIsoDateString(this.stateObj)}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._dateChanged}
@@ -58,7 +41,7 @@ class MoreInfoInputDatetime extends LitElement {
: this.stateObj.attributes.has_date
? this.stateObj.state.split(" ")[1]
: this.stateObj.state}
.locale=${this._locale}
.locale=${this.hass.locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
@@ -74,7 +57,7 @@ class MoreInfoInputDatetime extends LitElement {
private _timeChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this._api.callService,
this.hass!,
this.stateObj!.entity_id,
ev.detail.value,
this.stateObj!.attributes.has_date
@@ -85,7 +68,7 @@ class MoreInfoInputDatetime extends LitElement {
private _dateChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this._api.callService,
this.hass!,
this.stateObj!.entity_id,
this.stateObj!.attributes.has_time
? this.stateObj!.state.split(" ")[1]
@@ -60,6 +60,7 @@ class MoreInfoLight extends LitElement {
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
@@ -1,34 +1,20 @@
import { consume } 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 { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-select";
import { apiContext, formattersContext } from "../../../data/context";
import type { RemoteEntity } from "../../../data/remote";
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
import type { HomeAssistantApi, HomeAssistantFormatters } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-remote")
class MoreInfoRemote extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: RemoteEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this._localize || !this._formatters || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -38,14 +24,14 @@ class MoreInfoRemote extends LitElement {
${supportsFeature(stateObj, REMOTE_SUPPORT_ACTIVITY)
? html`
<ha-select
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.more_info_control.remote.activity"
)}
.value=${stateObj.attributes.current_activity || ""}
@selected=${this._handleActivityChanged}
.options=${stateObj.attributes.activity_list?.map((activity) => ({
value: activity,
label: this._formatters.formatEntityAttributeValue(
label: this.hass!.formatEntityAttributeValue(
stateObj,
"activity",
activity
@@ -66,7 +52,7 @@ class MoreInfoRemote extends LitElement {
return;
}
this._api.callService("remote", "turn_on", {
this.hass.callService("remote", "turn_on", {
entity_id: this.stateObj!.entity_id,
activity: newVal,
});
@@ -2,11 +2,10 @@ import { mdiVolumeHigh, mdiVolumeOff } 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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { customElement, property } from "lit/decorators";
import "../../../state-control/ha-state-control-toggle";
import "../../../components/ha-button";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -15,12 +14,12 @@ import { showSirenAdvancedControlsView } from "../components/siren/show-dialog-s
@customElement("more-info-siren")
class MoreInfoSiren extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -40,11 +39,13 @@ class MoreInfoSiren extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls">
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiVolumeHigh}
.iconPathOff=${mdiVolumeOff}
></ha-state-control-toggle>
@@ -54,7 +55,7 @@ class MoreInfoSiren extends LitElement {
size="s"
@click=${this._showAdvancedControlsDialog}
>
${this._localize("ui.components.siren.advanced_controls")}
${this.hass.localize("ui.components.siren.advanced_controls")}
</ha-button>`
: nothing}
</div>
@@ -2,31 +2,32 @@ import { mdiPower, mdiPowerOff } 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 { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { customElement, property } from "lit/decorators";
import "../../../state-control/ha-state-control-toggle";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-switch")
class MoreInfoSwitch extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls">
<ha-state-control-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiPower}
.iconPathOff=${mdiPowerOff}
></ha-state-control-toggle>
@@ -1,35 +1,18 @@
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 { transform } from "../../../common/decorators/transform";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
import type { FrontendLocaleData } from "../../../data/translation";
import type {
HomeAssistantApi,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-time")
class MoreInfoTime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
return nothing;
@@ -40,7 +23,7 @@ class MoreInfoTime extends LitElement {
.value=${this.stateObj.state === UNKNOWN
? undefined
: this.stateObj.state}
.locale=${this._locale}
.locale=${this.hass.locale}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>
@@ -53,11 +36,7 @@ class MoreInfoTime extends LitElement {
private _timeChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
setTimeValue(
this._api.callService,
this.stateObj!.entity_id,
ev.detail.value
);
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
}
@@ -1,81 +1,23 @@
import { consume } from "@lit/context";
import type { PropertyValues } from "lit";
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 { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import "../../../components/ha-duration-input";
import type { HaDurationData } from "../../../components/ha-duration-input";
import { apiContext } from "../../../data/context";
import type { TimerEntity } from "../../../data/timer";
import { timerDurationData } from "../../../data/timer";
import type { HomeAssistantApi, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-timer")
class MoreInfoTimer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: TimerEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state() private _duration?: HaDurationData;
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
// Seed the field once from the configured duration and keep it static,
// so it never jumps to the live remaining time as the timer ticks.
if (this._duration === undefined && this.stateObj) {
this._duration = timerDurationData(this.stateObj);
}
}
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
const timerState = this.stateObj.state;
return html`
<ha-duration-input
.data=${this._duration}
required
@value-changed=${this._durationChanged}
></ha-duration-input>
<div class="actions">
${timerState === "idle"
? html`
<ha-button appearance="plain" size="s" @click=${this._start}>
${this._localize("ui.card.timer.actions.start")}
</ha-button>
`
: nothing}
${timerState === "active" || timerState === "paused"
? html`
<ha-button appearance="plain" size="s" @click=${this._start}>
${this._localize("ui.card.timer.actions.set")}
</ha-button>
`
: nothing}
${timerState === "active"
? html`
<ha-button
appearance="plain"
size="s"
.action=${"pause"}
@click=${this._handleActionClick}
>
${this._localize("ui.card.timer.actions.pause")}
</ha-button>
`
: nothing}
${timerState === "paused"
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
? html`
<ha-button
appearance="plain"
@@ -83,11 +25,23 @@ class MoreInfoTimer extends LitElement {
.action=${"start"}
@click=${this._handleActionClick}
>
${this._localize("ui.card.timer.actions.start")}
${this.hass!.localize("ui.card.timer.actions.start")}
</ha-button>
`
: nothing}
${timerState === "active" || timerState === "paused"
: ""}
${this.stateObj.state === "active"
? html`
<ha-button
appearance="plain"
size="s"
.action=${"pause"}
@click=${this._handleActionClick}
>
${this.hass!.localize("ui.card.timer.actions.pause")}
</ha-button>
`
: ""}
${this.stateObj.state === "active" || this.stateObj.state === "paused"
? html`
<ha-button
appearance="plain"
@@ -95,7 +49,7 @@ class MoreInfoTimer extends LitElement {
.action=${"cancel"}
@click=${this._handleActionClick}
>
${this._localize("ui.card.timer.actions.cancel")}
${this.hass!.localize("ui.card.timer.actions.cancel")}
</ha-button>
<ha-button
appearance="plain"
@@ -103,48 +57,26 @@ class MoreInfoTimer extends LitElement {
.action=${"finish"}
@click=${this._handleActionClick}
>
${this._localize("ui.card.timer.actions.finish")}
${this.hass!.localize("ui.card.timer.actions.finish")}
</ha-button>
`
: nothing}
: ""}
</div>
`;
}
private _durationChanged(
ev: ValueChangedEvent<HaDurationData | undefined>
): void {
this._duration = ev.detail.value;
}
// Used by idle "Start" and active/paused "Set": (re)starts the timer with the
// entered duration. timer.start has no upper bound, so values beyond the
// configured duration are accepted.
private _start(): void {
this._api.callService("timer", "start", {
entity_id: this.stateObj!.entity_id,
...(this._duration ? { duration: this._duration } : {}),
});
}
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
this._api.callService("timer", action, {
this.hass.callService("timer", action, {
entity_id: this.stateObj!.entity_id,
});
}
static styles = css`
ha-duration-input {
display: flex;
justify-content: center;
margin: var(--ha-space-4) 0 var(--ha-space-2);
}
.actions {
margin: var(--ha-space-2) 0;
display: flex;
flex-wrap: wrap;
gap: var(--ha-space-2);
justify-content: center;
}
`;
@@ -1,14 +1,9 @@
import { consume } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { BINARY_STATE_OFF } from "../../../common/const";
import { relativeTime } from "../../../common/datetime/relative_time";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -20,18 +15,10 @@ import "../../../components/item/ha-row-item";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import {
apiContext,
configContext,
formattersContext,
internationalizationContext,
statesContext,
} from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { EntitySources } from "../../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
import type { FrontendLocaleData } from "../../../data/translation";
import type { UpdateEntity, UpdateType } from "../../../data/update";
import {
getUpdateType,
@@ -41,49 +28,15 @@ import {
updateIsInstalling,
updateReleaseNotes,
} from "../../../data/update";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: UpdateEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HomeAssistant["states"];
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config!: HassConfig;
@state() private _releaseNotes?: string | null;
@state() private _error?: string;
@@ -98,7 +51,7 @@ class MoreInfoUpdate extends LitElement {
private async _fetchBackupConfig() {
try {
const { config } = await fetchBackupConfig(this._api);
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
} catch (err) {
// ignore error, because user will get a manual backup option
@@ -109,7 +62,7 @@ class MoreInfoUpdate extends LitElement {
private async _fetchUpdateBackupConfig(type: UpdateType) {
try {
const config = await getSupervisorUpdateConfig(this._api);
const config = await getSupervisorUpdateConfig(this.hass);
// for home assistant and OS updates
if (this._isHaOrOsUpdate(type)) {
@@ -128,10 +81,7 @@ class MoreInfoUpdate extends LitElement {
}
private async _fetchEntitySources() {
this._entitySources = await fetchEntitySourcesWithCache({
callWS: this._api.callWS,
states: this._states,
});
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
}
private _isHaOrOsUpdate(type: UpdateType): boolean {
@@ -161,10 +111,10 @@ class MoreInfoUpdate extends LitElement {
if (!isBackupConfigValid) {
return {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual"
),
description: this._localize(
description: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual_description"
),
};
@@ -177,22 +127,22 @@ class MoreInfoUpdate extends LitElement {
const now = new Date();
return {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic"
),
description: lastAutomaticBackupDate
? this._localize(
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
{
relative_time: relativeTime(
lastAutomaticBackupDate,
this._locale,
this.hass.locale,
now,
true
),
}
)
: this._localize(
: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
),
};
@@ -202,11 +152,11 @@ class MoreInfoUpdate extends LitElement {
if (updateType === "addon") {
const version = this.stateObj.attributes.installed_version;
return {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.app"
),
description: version
? this._localize(
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.app_description",
{ version: version }
)
@@ -216,7 +166,7 @@ class MoreInfoUpdate extends LitElement {
// Fallback to generic UI
return {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.generic"
),
};
@@ -224,7 +174,7 @@ class MoreInfoUpdate extends LitElement {
protected render() {
if (
!this._localize ||
!this.hass ||
!this.stateObj ||
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
@@ -252,26 +202,26 @@ class MoreInfoUpdate extends LitElement {
: nothing}
<div class="row">
<div class="key">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this._localize("state.default.unavailable")}
this.hass.localize("state.default.unavailable")}
</div>
</div>
<div class="row">
<div class="key">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this._localize("state.default.unavailable")}
this.hass.localize("state.default.unavailable")}
</div>
</div>
@@ -283,7 +233,7 @@ class MoreInfoUpdate extends LitElement {
target="_blank"
rel="noreferrer"
>
${this._localize(
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
@@ -350,7 +300,7 @@ class MoreInfoUpdate extends LitElement {
appearance="plain"
@click=${this._handleClearSkipped}
>
${this._localize(
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</ha-button>
@@ -363,7 +313,9 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this._localize("ui.dialogs.more_info_control.update.skip")}
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</ha-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
@@ -373,7 +325,7 @@ class MoreInfoUpdate extends LitElement {
.loading=${updateIsInstalling(this.stateObj)}
.disabled=${updateButtonIsDisabled(this.stateObj)}
>
${this._localize(
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"
)}
</ha-button>
@@ -400,7 +352,7 @@ class MoreInfoUpdate extends LitElement {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (
isComponentLoaded(this._config, "hassio") &&
isComponentLoaded(this.hass.config, "hassio") &&
["addon", "home_assistant", "home_assistant_os"].includes(type)
) {
this._fetchUpdateBackupConfig(type);
@@ -422,7 +374,7 @@ class MoreInfoUpdate extends LitElement {
private async _fetchReleaseNotes() {
try {
this._releaseNotes = await updateReleaseNotes(
this._api,
this.hass,
this.stateObj!.entity_id
);
} catch (err: any) {
@@ -453,7 +405,7 @@ class MoreInfoUpdate extends LitElement {
installData.version = this.stateObj!.attributes.latest_version;
}
this._api.callService("update", "install", installData);
this.hass.callService("update", "install", installData);
}
private _createBackupChanged(ev) {
@@ -463,22 +415,22 @@ class MoreInfoUpdate extends LitElement {
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
),
text: this._localize(
text: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
),
});
return;
}
this._api.callService("update", "skip", {
this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id,
});
}
private _handleClearSkipped(): void {
this._api.callService("update", "clear_skipped", {
this.hass.callService("update", "clear_skipped", {
entity_id: this.stateObj!.entity_id,
});
}
@@ -1,14 +1,10 @@
import { consume } from "@lit/context";
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import { formattersContext } from "../../../data/context";
import {
shouldShowFavoriteOptions,
type ExtEntityRegistryEntry,
@@ -22,7 +18,7 @@ import {
import "../../../state-control/valve/ha-state-control-valve-buttons";
import "../../../state-control/valve/ha-state-control-valve-position";
import "../../../state-control/valve/ha-state-control-valve-toggle";
import type { HomeAssistantFormatters } from "../../../types";
import type { HomeAssistant } from "../../../types";
import "../components/valves/ha-more-info-valve-favorite-positions";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@@ -31,13 +27,7 @@ type Mode = "position" | "button";
@customElement("more-info-valve")
class MoreInfoValve extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ValveEntity;
@@ -65,11 +55,11 @@ class MoreInfoValve extends LitElement {
}
private get _stateOverride() {
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const positionStateDisplay = computeValvePositionStateDisplay(
this.stateObj!,
this._formatters.formatEntityAttributeValue
this.hass
);
if (positionStateDisplay) {
@@ -79,7 +69,7 @@ class MoreInfoValve extends LitElement {
}
protected render() {
if (!this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -107,6 +97,7 @@ class MoreInfoValve extends LitElement {
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
@@ -119,6 +110,7 @@ class MoreInfoValve extends LitElement {
? html`
<ha-state-control-valve-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-position>
`
: nothing}
@@ -132,12 +124,14 @@ class MoreInfoValve extends LitElement {
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-toggle>
`
: supportsOpenClose
? html`
<ha-state-control-valve-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-buttons>
`
: nothing}
@@ -150,7 +144,7 @@ class MoreInfoValve extends LitElement {
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this._localize(
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.position`
)}
.selected=${this._mode === "position"}
@@ -159,7 +153,7 @@ class MoreInfoValve extends LitElement {
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this._localize(
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.button`
)}
.selected=${this._mode === "button"}
@@ -176,6 +170,7 @@ class MoreInfoValve extends LitElement {
showFavoriteControls
? html`
<ha-more-info-valve-favorite-positions
.hass=${this.hass}
.stateObj=${this.stateObj}
.entry=${this.entry}
.editMode=${this.editMode}
@@ -1,12 +1,8 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiAccount, mdiAccountArrowRight, mdiWaterBoiler } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -17,29 +13,20 @@ import {
WaterHeaterEntityFeature,
compareWaterHeaterOperationMode,
} from "../../../data/water_heater";
import { apiContext, formattersContext } from "../../../data/context";
import "../../../state-control/water_heater/ha-state-control-water_heater-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-water_heater")
class MoreInfoWaterHeater extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="operation_mode"
.attributeValue=${value}
@@ -70,13 +57,13 @@ class MoreInfoWaterHeater extends LitElement {
? html`
<div>
<p class="label">
${this._formatters.formatEntityAttributeName(
${this.hass.formatEntityAttributeName(
this.stateObj,
"current_temperature"
)}
</p>
<p class="value">
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}
@@ -87,6 +74,7 @@ class MoreInfoWaterHeater extends LitElement {
</div>
<div class="controls">
<ha-state-control-water_heater-temperature
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-water_heater-temperature>
</div>
@@ -94,7 +82,8 @@ class MoreInfoWaterHeater extends LitElement {
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.label=${this._localize("ui.card.water_heater.mode")}
.hass=${this.hass}
.label=${this.hass.localize("ui.card.water_heater.mode")}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
@wa-select=${this._handleOperationModeChanged}
@@ -103,7 +92,7 @@ class MoreInfoWaterHeater extends LitElement {
.sort(compareWaterHeaterOperationMode)
.map((mode) => ({
value: mode,
label: this._formatters.formatEntityState(stateObj, mode),
label: this.hass.formatEntityState(stateObj, mode),
}))}
.renderIcon=${this._renderOperationModeIcon}
>
@@ -114,7 +103,7 @@ class MoreInfoWaterHeater extends LitElement {
${supportAwayMode
? html`
<ha-control-select-menu
.label=${this._formatters.formatEntityAttributeName(
.label=${this.hass.formatEntityAttributeName(
stateObj,
"away_mode"
)}
@@ -123,7 +112,7 @@ class MoreInfoWaterHeater extends LitElement {
@wa-select=${this._handleAwayModeChanged}
.options=${["on", "off"].map((mode) => ({
value: mode,
label: this._formatters.formatEntityAttributeValue(
label: this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
mode
@@ -176,7 +165,7 @@ class MoreInfoWaterHeater extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this._api.callService("water_heater", service, data);
await this.hass.callService("water_heater", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
+4 -36
View File
@@ -13,11 +13,6 @@ import { computeShownAttributes } from "../../data/entity/entity_attributes";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
import { computeDomain } from "../../common/entity/compute_domain";
import type { FeatureEnum } from "../../common/entity/get_domain_features";
import { getFeatures } from "../../common/entity/get_domain_features";
import { supportsFeature } from "../../common/entity/supports-feature";
import { titleCase } from "../../common/string/title-case";
interface DetailsViewParams {
entityId: string;
@@ -182,12 +177,6 @@ class HaMoreInfoDetails extends LitElement {
</div>`;
}
let featureEnum: FeatureEnum | undefined;
if (this._stateObj?.attributes.supported_features !== undefined) {
const domain = computeDomain(this.params!.entityId);
featureEnum = getFeatures(domain);
}
return attributes.map(
(attribute) => html`
<div class="data-entry">
@@ -200,37 +189,16 @@ class HaMoreInfoDetails extends LitElement {
)}
</div>
<div class="value">
${attribute === "supported_features" && featureEnum
? this._renderFeatures(featureEnum, this._stateObj!)
: html`
<ha-attribute-value
.attribute=${attribute}
.stateObj=${this._stateObj}
></ha-attribute-value>
`}
<ha-attribute-value
.attribute=${attribute}
.stateObj=${this._stateObj}
></ha-attribute-value>
</div>
</div>
`
);
}
private _renderFeatures(
featureEnum: FeatureEnum,
stateObj: HassEntity
): string {
return (
Object.entries(featureEnum)
.filter(([_key, value]) => typeof value === "number")
.map(([key, value]) =>
supportsFeature(stateObj, value as number)
? titleCase(key.replaceAll("_", "\u00A0").toLowerCase())
: undefined
)
.filter(Boolean)
.join(", ") || this.hass.localize("ui.common.none")
);
}
static styles: CSSResultGroup = css`
:host {
display: flex;
@@ -42,10 +42,11 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._entityIdAsList(this.entityId)}
.scope=${"entity"}
narrow
no-icon
graph-color
no-name
show-indicator
relative-time
></ha-logbook>
`;
}
+1 -1
View File
@@ -170,7 +170,7 @@ class DialogRestart extends LitElement {
</ha-list-base>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.restart.more_options"
"ui.dialogs.restart.advanced_options"
)}
>
<ha-list-base>
+13 -2
View File
@@ -2,6 +2,7 @@ import { mdiOpenInNew, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
@@ -57,9 +58,14 @@ class HaConfigAppsInfo extends LitElement {
</h2>
<p>
${this.hass.localize(
"ui.panel.config.apps.info.installation_hint"
"ui.panel.config.apps.info.why_not_available_description"
)}
</p>
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.apps.info.installation_hint"
)}
</ha-alert>
</div>
<div class="card-actions">
<ha-button
@@ -138,11 +144,16 @@ class HaConfigAppsInfo extends LitElement {
}
p {
margin: 0;
margin: 0 0 var(--ha-space-3);
line-height: var(--ha-line-height-normal);
color: var(--secondary-text-color);
}
ha-alert {
display: block;
margin-top: var(--ha-space-2);
}
.card-actions {
display: flex;
justify-content: space-between;
+1 -38
View File
@@ -1,7 +1,5 @@
import { startOfYesterday } from "date-fns";
import { consume } from "@lit/context";
import {
mdiChevronRight,
mdiDelete,
mdiDevices,
mdiDotsVertical,
@@ -32,7 +30,6 @@ import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { goBack, navigate } from "../../../common/navigate";
import { createSearchParam } from "../../../common/url/search-params";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
@@ -599,30 +596,12 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
const logbookColumn = html`
${isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card outlined>
<div class="card-header logbook-header">
<span>${this.hass.localize("panel.logbook")}</span>
<a
href="/logbook?${createSearchParam({
area_id: this.areaId,
start_date: startOfYesterday().toISOString(),
back: "1",
})}"
>
<ha-icon-button
.path=${mdiChevronRight}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}
></ha-icon-button>
</a>
</div>
<ha-card outlined .header=${this.hass.localize("panel.logbook")}>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
.scope=${"area"}
virtualize
narrow
no-icon
@@ -1000,22 +979,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
opacity: 0.5;
border-radius: var(--ha-border-radius-circle);
}
.logbook-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--ha-space-4) var(--ha-space-4) 0;
}
.logbook-header a {
display: flex;
align-items: center;
color: var(--primary-text-color);
margin-right: calc(var(--ha-space-2) * -1);
margin-inline-end: calc(var(--ha-space-2) * -1);
margin-inline-start: initial;
}
ha-logbook {
height: 400px;
}
@@ -52,7 +52,6 @@ export class HaWaitForTriggerAction
{
name: "continue_on_timeout",
selector: { boolean: {} },
default: true,
},
] as const satisfies readonly HaFormSchema[]
);
@@ -21,7 +21,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): WaitAction {
return { wait_template: "" };
return { wait_template: "", continue_on_timeout: true };
}
private _schema = memoizeOne(
@@ -44,7 +44,6 @@ export class HaWaitAction extends LitElement implements ActionElement {
{
name: "continue_on_timeout",
selector: { boolean: {} },
default: true,
},
] as const satisfies readonly HaFormSchema[]
);
@@ -1,6 +1,7 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -34,32 +35,40 @@ const stateConditionStruct = object({
enabled: optional(boolean()),
});
const SCHEMA = [
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "attribute",
selector: {
attribute: {
hide_attributes: STATE_CONDITION_HIDDEN_ATTRIBUTES,
const SCHEMA = memoizeOne(
(hasAttribute: boolean) =>
[
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "attribute",
selector: {
attribute: {
hide_attributes: STATE_CONDITION_HIDDEN_ATTRIBUTES,
},
},
context: {
filter_entity: "entity_id",
},
},
},
context: {
filter_entity: "entity_id",
},
},
{
name: "state",
required: true,
selector: {
state: { multiple: true },
},
context: {
filter_entity: "entity_id",
filter_attribute: "attribute",
},
},
{ name: "for", selector: { duration: {} } },
] as const;
{
name: "state",
required: true,
selector: {
state: { multiple: true },
},
context: {
filter_entity: "entity_id",
filter_attribute: "attribute",
},
},
// `for` is not supported together with `attribute`: the legacy state
// condition measures the duration against `last_changed`, which only
// updates on state changes, not attribute changes.
...(hasAttribute
? ([] as const)
: ([{ name: "for", selector: { duration: {} } }] as const)),
] as const
);
@customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement {
@@ -86,6 +95,7 @@ export class HaStateCondition extends LitElement implements ConditionElement {
}
protected render() {
const hasAttribute = !!this.condition.attribute;
const trgFor = createDurationData(this.condition.for);
const data = {
...this.condition,
@@ -97,10 +107,11 @@ export class HaStateCondition extends LitElement implements ConditionElement {
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${SCHEMA}
.schema=${SCHEMA(hasAttribute)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
></ha-form>
`;
}
@@ -115,6 +126,12 @@ export class HaStateCondition extends LitElement implements ConditionElement {
: {}
);
// `for` is not supported together with `attribute` for the legacy state
// condition, so drop any lingering duration when an attribute is set.
if (newCondition.attribute) {
delete newCondition.for;
}
// Ensure `state` stays an array for multi-select. If absent, set to []
if (newCondition.state === undefined || newCondition.state === "") {
newCondition.state = [];
@@ -124,7 +141,7 @@ export class HaStateCondition extends LitElement implements ConditionElement {
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string => {
switch (schema.name) {
case "entity_id":
@@ -144,6 +161,17 @@ export class HaStateCondition extends LitElement implements ConditionElement {
}
};
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string | undefined => {
if (schema.name === "attribute" && this.condition.attribute) {
return this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.state.attribute_no_for"
);
}
return undefined;
};
static styles = css`
:host {
display: block;
@@ -269,9 +269,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const category = entityRegEntry?.categories.automation;
const labels = labelReg && entityRegEntry?.labels;
const label_entries = (labels || [])
.map((lbl) => labelLookup!.get(lbl))
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
.map((lbl) => labelLookup!.get(lbl)!)
.filter(Boolean);
const assistants = getEntityVoiceAssistantsIds(
entityReg,
automation.entity_id
@@ -313,7 +313,7 @@ class HaBackupConfigData extends LitElement {
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.data.share_folder_desc"
"ui.panel.config.backup.data.share_folder_description"
)}
</span>
<ha-switch
@@ -1,9 +1,7 @@
import { startOfYesterday } from "date-fns";
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiCog,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiDownload,
@@ -100,7 +98,6 @@ import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isHelperDomain } from "../helpers/const";
import { createSearchParam } from "../../../common/url/search-params";
import { brandsUrl } from "../../../util/brands-url";
import { fileDownload } from "../../../util/file_download";
import "../../logbook/ha-logbook";
@@ -903,29 +900,12 @@ export class HaConfigDevicePage extends LitElement {
const logbookColumn = isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card outlined>
<div class="card-header">
<span>${this.hass.localize("panel.logbook")}</span>
<a
href="/logbook?${createSearchParam({
device_id: this.deviceId,
start_date: startOfYesterday().toISOString(),
back: "1",
})}"
>
<ha-icon-button
.path=${mdiChevronRight}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}
></ha-icon-button>
</a>
</div>
<h1 class="card-header">${this.hass.localize("panel.logbook")}</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
.scope=${"device"}
virtualize
narrow
no-icon
@@ -1802,22 +1782,6 @@ export class HaConfigDevicePage extends LitElement {
display: block;
}
ha-card:has(ha-logbook) .card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--ha-space-4) var(--ha-space-4) 0;
}
ha-card:has(ha-logbook) .card-header a {
display: flex;
align-items: center;
color: var(--primary-text-color);
margin-right: calc(var(--ha-space-2) * -1);
margin-inline-end: calc(var(--ha-space-2) * -1);
margin-inline-start: initial;
}
ha-card:has(ha-logbook) {
padding-bottom: var(
--ha-card-border-radius,
@@ -231,7 +231,6 @@ export class EntitySettingsHelperTab extends LitElement {
}
.form {
padding: 20px 24px;
z-index: 0;
}
.buttons {
box-sizing: border-box;
@@ -239,9 +238,6 @@ export class EntitySettingsHelperTab extends LitElement {
justify-content: space-between;
padding: 16px;
background-color: var(--mdc-theme-surface, #fff);
position: sticky;
bottom: 0px;
z-index: 1;
}
.error {
color: var(--error-color);
@@ -688,9 +688,9 @@ export class HaConfigEntities extends LitElement {
}
const labels = labelReg && entry?.labels;
const labelsEntries = (labels || [])
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
const labelsEntries = (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
const entityName = computeEntityEntryName(
entry as EntityRegistryEntry,
@@ -44,7 +44,7 @@ class DialogScheduleBlockInfo extends DirtyStateProviderMixin<ScheduleBlockInfo>
selector: { time: { no_second: true } },
},
{
name: "more_options",
name: "advanced_settings",
type: "expandable" as const,
flatten: true,
expanded: expand,
@@ -157,9 +157,9 @@ class DialogScheduleBlockInfo extends DirtyStateProviderMixin<ScheduleBlockInfo>
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
case "data":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.data");
case "more_options":
case "advanced_settings":
return this.hass!.localize(
"ui.dialogs.helper_settings.generic.more_options"
"ui.dialogs.helper_settings.generic.advanced_settings"
);
}
return "";
@@ -125,7 +125,7 @@ class HaCounterForm extends LitElement {
></ha-input>
<ha-expansion-panel
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.more_options"
"ui.dialogs.helper_settings.generic.advanced_settings"
)}
outlined
>
@@ -93,7 +93,7 @@ class HaInputTextForm extends LitElement {
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.more_options"
"ui.dialogs.helper_settings.generic.advanced_settings"
)}
outlined
>
@@ -552,9 +552,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const entityRegEntry =
entityRegistryByEntityId(entityReg)[item.entity_id];
const labels = labelReg && entityRegEntry?.labels;
const label_entries = (labels || [])
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
const label_entries = (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
const category = entityRegEntry?.categories.helpers;
const deviceId = entityRegEntry?.device_id;
const areaId =
@@ -244,9 +244,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
);
const category = entityRegEntry?.categories.scene;
const labels = labelReg && entityRegEntry?.labels;
const label_entries = (labels || [])
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
const label_entries = (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
const assistants = getEntityVoiceAssistantsIds(
entityReg,
scene.entity_id
+3 -3
View File
@@ -250,9 +250,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
);
const category = entityRegEntry?.categories.script;
const labels = labelReg && entityRegEntry?.labels;
const label_entries = (labels || [])
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
const label_entries = (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
const assistants = getEntityVoiceAssistantsIds(
entityReg,
script.entity_id

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