mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-18 06:11:50 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e949a4974d | |||
| 80bcbc8a8c | |||
| a5bf35690b | |||
| d98eb47490 | |||
| 738e92d27d | |||
| ade2e9272b | |||
| d8ce60dfb6 | |||
| db9374925e | |||
| 1bcd1293c0 | |||
| b8cf061ebb | |||
| 6585da9a73 | |||
| 368df82e97 | |||
| 1d99a5dff9 | |||
| 0ca72b763a | |||
| 31848a1efd | |||
| c6f79c2093 | |||
| 1a5ab1903a | |||
| a410a53524 | |||
| 012889e51d | |||
| 3b3788b722 | |||
| 9414bbc6ab | |||
| 287aabc9a3 | |||
| 2d505048c5 | |||
| e07cbb9164 |
@@ -0,0 +1,190 @@
|
||||
name: Pull request standards
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- ready_for_review
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check pull request follows contribution standards
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # To label and comment on pull requests
|
||||
steps:
|
||||
- name: Check pull request standards
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
||||
if (pr.user.type === "Bot") {
|
||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
||||
return;
|
||||
}
|
||||
if (pr.draft) {
|
||||
core.info("Skipping draft pull request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: pr.user.login,
|
||||
});
|
||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`${pr.user.login} is not an organization member, checking standards`);
|
||||
}
|
||||
|
||||
const label = "Needs Template";
|
||||
const marker = "<!-- pr-standards-check -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = pr.number;
|
||||
|
||||
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
||||
const ignoreMissing = async (fn) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info("Target already removed, nothing to do");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
||||
const setMinimized = async (subjectId, minimized) => {
|
||||
const mutation = minimized
|
||||
? `mutation($id: ID!) {
|
||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`
|
||||
: `mutation($id: ID!) {
|
||||
unminimizeComment(input: { subjectId: $id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await github.graphql(mutation, { id: subjectId });
|
||||
} catch (error) {
|
||||
core.info(
|
||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Content of a "## <name>" section, or null when the heading is absent.
|
||||
const section = (name) => {
|
||||
const match = body.match(
|
||||
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const problems = [];
|
||||
|
||||
const requiredHeadings = [
|
||||
"## proposed change",
|
||||
"## type of change",
|
||||
"## checklist",
|
||||
];
|
||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
||||
problems.push(
|
||||
"Use the pull request template without removing its sections."
|
||||
);
|
||||
}
|
||||
|
||||
const typeOfChange = section("type of change");
|
||||
if (typeOfChange !== null) {
|
||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
||||
if (ticked !== 1) {
|
||||
problems.push(
|
||||
'Select exactly one option under "Type of change".'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const proposedChange = section("proposed change");
|
||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
||||
problems.push('Describe your changes under "Proposed change".');
|
||||
}
|
||||
|
||||
const isValid = problems.length === 0;
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number, per_page: 100 }
|
||||
);
|
||||
const existing = comments.find((c) => c.body.includes(marker));
|
||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
||||
|
||||
if (isValid) {
|
||||
core.info("Pull request standards met");
|
||||
|
||||
if (hasLabel) {
|
||||
await ignoreMissing(() =>
|
||||
github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number, name: label,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existing) {
|
||||
await setMinimized(existing.node_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
`${marker}\n` +
|
||||
`Hey @${pr.user.login}!\n\n` +
|
||||
`Thank you for your contribution! To help reviewers, please update ` +
|
||||
`this pull request to follow our pull request standards:\n\n` +
|
||||
problems.map((p) => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body: message,
|
||||
});
|
||||
await setMinimized(existing.node_id, false);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number, body: message,
|
||||
});
|
||||
}
|
||||
|
||||
// Fail this check so it can block the PR from being merged
|
||||
core.setFailed(
|
||||
`Pull request standards not met:\n- ${problems.join("\n- ")}`
|
||||
);
|
||||
@@ -110,6 +110,15 @@ 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";
|
||||
|
||||
@@ -3,19 +3,20 @@ import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { selectUnit } from "../util/select-unit";
|
||||
|
||||
const formatRelTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
|
||||
(locale: FrontendLocaleData, style: Intl.RelativeTimeFormatStyle) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style })
|
||||
);
|
||||
|
||||
export const relativeTime = (
|
||||
from: Date,
|
||||
locale: FrontendLocaleData,
|
||||
to?: Date,
|
||||
includeTense = true
|
||||
includeTense = true,
|
||||
style: Intl.RelativeTimeFormatStyle = "long"
|
||||
): string => {
|
||||
const diff = selectUnit(from, to, locale);
|
||||
if (includeTense) {
|
||||
return formatRelTimeMem(locale).format(diff.value, diff.unit);
|
||||
return formatRelTimeMem(locale, style).format(diff.value, diff.unit);
|
||||
}
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export type FeatureClassNames<T extends number = number> = Partial<
|
||||
Record<T, string>
|
||||
>;
|
||||
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export const featureClassNames = (
|
||||
stateObj: HassEntity,
|
||||
classNames: FeatureClassNames
|
||||
) => {
|
||||
if (!stateObj || !stateObj.attributes.supported_features) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) =>
|
||||
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
}
|
||||
@@ -17,8 +17,6 @@ export type LocalizeKeys =
|
||||
| `ui.common.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
| `ui.components.logbook.messages.cleared_device_classes.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||
|
||||
@@ -16,14 +16,12 @@ interface CacheResult<T> {
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
*/
|
||||
export const timeCachePromiseFunc = async <T>(
|
||||
export const timeCachePromiseFunc = async <T, H = HomeAssistant>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
|
||||
generateCacheKey:
|
||||
| ((hass: HomeAssistant, lastResult: T) => unknown)
|
||||
| undefined,
|
||||
hass: HomeAssistant,
|
||||
func: (hass: H, ...args: any[]) => Promise<T>,
|
||||
generateCacheKey: ((hass: H, lastResult: T) => unknown) | undefined,
|
||||
hass: H,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
const anyHass = hass as any;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiCloseCircle,
|
||||
mdiHelpCircle,
|
||||
} from "@mdi/js";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -19,46 +26,59 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
private get _iconPath() {
|
||||
switch (this.state) {
|
||||
case "pass":
|
||||
return mdiCheckCircle;
|
||||
case "fail":
|
||||
return mdiCloseCircle;
|
||||
case "invalid":
|
||||
return mdiAlertCircle;
|
||||
default:
|
||||
return mdiHelpCircle;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
id="indicator"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
|
||||
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
top: -8px;
|
||||
inset-inline-end: -8px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: var(--ha-border-width-md) solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
#indicator ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
color: var(--ha-color-green-60);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-orange-60);
|
||||
color: var(--ha-color-orange-60);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-red-60);
|
||||
color: var(--ha-color-red-60);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -79,9 +79,11 @@ function computeTimelineEnumColor(
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const states =
|
||||
FIXED_DOMAIN_STATES[domain] ||
|
||||
(domain === "sensor" &&
|
||||
stateObj.attributes.device_class === "enum" &&
|
||||
stateObj.attributes.options) ||
|
||||
((domain === "sensor" && stateObj.attributes.device_class === "enum") ||
|
||||
domain === "select" ||
|
||||
domain === "input_select"
|
||||
? stateObj.attributes.options
|
||||
: undefined) ||
|
||||
[];
|
||||
const idx = states.indexOf(state);
|
||||
if (idx === -1) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiChevronDown,
|
||||
@@ -10,7 +11,9 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
type AssistPipeline,
|
||||
@@ -18,10 +21,19 @@ import {
|
||||
type ConversationChatLogToolResultDelta,
|
||||
type PipelineRunEvent,
|
||||
} from "../data/assist_pipeline";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import { ConversationEntityFeature } from "../data/conversation";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
} from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
@@ -47,8 +59,6 @@ interface AssistMessage {
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
@@ -71,6 +81,22 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: HomeAssistantConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: HomeAssistantConnection;
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
@@ -86,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
},
|
||||
@@ -124,9 +150,9 @@ export class HaAssistChat extends LitElement {
|
||||
const controlHA = !this.pipeline
|
||||
? false
|
||||
: this.pipeline.prefer_local_intents ||
|
||||
(this.hass.states[this.pipeline.conversation_engine]
|
||||
(this._states[this.pipeline.conversation_engine]
|
||||
? supportsFeature(
|
||||
this.hass.states[this.pipeline.conversation_engine],
|
||||
this._states[this.pipeline.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
)
|
||||
: true);
|
||||
@@ -139,7 +165,7 @@ export class HaAssistChat extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.conversation_no_control"
|
||||
)}
|
||||
</ha-alert>
|
||||
@@ -180,7 +206,7 @@ export class HaAssistChat extends LitElement {
|
||||
.path=${mdiCommentProcessingOutline}
|
||||
></ha-svg-icon>
|
||||
<span class="thinking-label">
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.show_details"
|
||||
)}
|
||||
</span>
|
||||
@@ -251,7 +277,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
id="message-input"
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._handleInput}
|
||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||
.label=${this._localize(`ui.dialogs.voice_command.input_label`)}
|
||||
>
|
||||
<div slot="end">
|
||||
${this._showSendButton || !supportsSTT
|
||||
@@ -261,7 +287,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiSend}
|
||||
@click=${this._handleSendMessage}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.send_text"
|
||||
)}
|
||||
>
|
||||
@@ -282,7 +308,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._handleListeningButton}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
@@ -391,21 +417,21 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this.hass.localize(
|
||||
html`${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
this._config,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
>${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
@@ -443,7 +469,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event: PipelineRunEvent) => {
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
@@ -539,7 +565,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
}
|
||||
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
this._connection.connection.socket!.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this._stt_binary_handler_id == undefined) {
|
||||
@@ -550,7 +576,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
data[0] = this._stt_binary_handler_id;
|
||||
data.set(new Uint8Array(chunk.buffer), 1);
|
||||
|
||||
this.hass.connection.socket!.send(data);
|
||||
this._connection.connection.socket!.send(data);
|
||||
}
|
||||
|
||||
private _unloadAudio = () => {
|
||||
@@ -570,7 +596,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
hassMessageProcesser.addMessage();
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event) => {
|
||||
if (event.type.startsWith("intent-")) {
|
||||
hassMessageProcesser.processEvent(event);
|
||||
@@ -593,7 +619,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
);
|
||||
} catch {
|
||||
hassMessageProcesser.setError(
|
||||
this.hass.localize("ui.dialogs.voice_command.error")
|
||||
this._localize("ui.dialogs.voice_command.error")
|
||||
);
|
||||
} finally {
|
||||
this._processing = false;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
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 } from "lit/decorators";
|
||||
import { customElement, property, state } 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;
|
||||
@@ -19,6 +23,18 @@ 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>`;
|
||||
@@ -28,12 +44,14 @@ export class HaAttributeIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const icon = attributeIcon(
|
||||
this.hass,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this._entities,
|
||||
this.stateObj,
|
||||
this.attribute,
|
||||
this.attributeValue
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@@ -15,17 +17,16 @@ export class HaBigNumber extends LitElement {
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render() {
|
||||
const formatted = formatNumber(
|
||||
this.value,
|
||||
this.hass?.locale,
|
||||
this.formatOptions
|
||||
);
|
||||
const locale = this._i18n!.locale;
|
||||
const formatted = formatNumber(this.value, locale, this.formatOptions);
|
||||
const [integer] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
@@ -33,9 +34,7 @@ export class HaBigNumber extends LitElement {
|
||||
const temperatureDecimal = formatted.replace(integer, "");
|
||||
|
||||
const formattedValue = `${this.value}${
|
||||
this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
|
||||
: ""
|
||||
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
|
||||
}`;
|
||||
|
||||
const unitBottom = this.unitPosition === "bottom";
|
||||
|
||||
@@ -101,15 +101,9 @@ export class HaLabelsPicker extends LitElement {
|
||||
language: string
|
||||
) =>
|
||||
value
|
||||
?.map(
|
||||
(id) =>
|
||||
labels?.find((label) => label.label_id === id) || {
|
||||
label_id: id,
|
||||
name: id,
|
||||
color: "rgba(var(--rgb-primary-text-color), 0.15)",
|
||||
}
|
||||
)
|
||||
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
|
||||
?.map((id) => labels?.find((label) => label.label_id === id))
|
||||
.filter((label): label is LabelRegistryEntry => label !== undefined)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.map((label) => ({
|
||||
...label,
|
||||
style: getLabelColorStyle(label.color),
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { HomeAssistantInternationalization } from "../types";
|
||||
class HaRelativeTime extends ReactiveElement {
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@property() public format: Intl.RelativeTimeFormatStyle = "long";
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
@state()
|
||||
@@ -36,13 +38,15 @@ class HaRelativeTime extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
super.update(changedProps);
|
||||
if (changedProps.has("datetime")) {
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
@@ -66,15 +70,23 @@ class HaRelativeTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
|
||||
this.textContent = this._i18n.localize(
|
||||
"ui.components.relative_time.never"
|
||||
);
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this._i18n.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
const relTime = relativeTime(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
undefined,
|
||||
true,
|
||||
this.format
|
||||
);
|
||||
this.textContent = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-time-format-picker";
|
||||
|
||||
@customElement("ha-selector-ui_time_format")
|
||||
export class HaSelectorUiTimeFormat extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-format-picker
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-time-format-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ const LOAD_ELEMENTS = {
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
ui_time_format: () => import("./ha-selector-ui-time-format"),
|
||||
};
|
||||
|
||||
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ 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,7 +388,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
return entries.length
|
||||
? html`
|
||||
<ha-logbook-renderer
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
|
||||
@@ -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 { localizeTriggerDescription } from "../../data/logbook";
|
||||
import { localizeTriggerSource } 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: localizeTriggerDescription(
|
||||
trigger: localizeTriggerSource(
|
||||
this.hass.localize,
|
||||
this.trace.trigger
|
||||
),
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Selector } from "./selector";
|
||||
|
||||
export const enum AITaskEntityFeature {
|
||||
export enum AITaskEntityFeature {
|
||||
GENERATE_DATA = 1,
|
||||
SUPPORT_ATTACHMENTS = 2,
|
||||
GENERATE_IMAGE = 4,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
|
||||
export const enum AlarmControlPanelEntityFeature {
|
||||
export enum AlarmControlPanelEntityFeature {
|
||||
ARM_HOME = 1,
|
||||
ARM_AWAY = 2,
|
||||
ARM_NIGHT = 4,
|
||||
|
||||
@@ -338,7 +338,7 @@ export const runDebugAssistPipeline = (
|
||||
};
|
||||
|
||||
export const runAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "connection">,
|
||||
callback: (event: PipelineRunEvent) => void,
|
||||
options: PipelineRunOptions
|
||||
) =>
|
||||
@@ -379,7 +379,10 @@ export const listAssistPipelines = (hass: HomeAssistant) =>
|
||||
type: "assist_pipeline/pipeline/list",
|
||||
});
|
||||
|
||||
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
|
||||
export const getAssistPipeline = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
pipeline_id?: string
|
||||
) =>
|
||||
hass.callWS<AssistPipeline>({
|
||||
type: "assist_pipeline/pipeline/get",
|
||||
pipeline_id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum AssistSatelliteEntityFeature {
|
||||
export enum AssistSatelliteEntityFeature {
|
||||
ANNOUNCE = 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ export const expandConditionWithShorthand = (
|
||||
};
|
||||
|
||||
export const triggerAutomationActions = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService">,
|
||||
entityId: string
|
||||
) => {
|
||||
hass.callService("automation", "trigger", {
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ export interface RestoreBackupParams {
|
||||
restore_homeassistant?: boolean;
|
||||
}
|
||||
|
||||
export const fetchBackupConfig = (hass: HomeAssistant) =>
|
||||
export const fetchBackupConfig = (hass: Pick<HomeAssistant, "callWS">) =>
|
||||
hass.callWS<{ config: BackupConfig }>({ type: "backup/config/info" });
|
||||
|
||||
export const updateBackupConfig = (
|
||||
|
||||
@@ -54,7 +54,7 @@ export enum RecurrenceRange {
|
||||
THISANDFUTURE = "THISANDFUTURE",
|
||||
}
|
||||
|
||||
export const enum CalendarEntityFeature {
|
||||
export enum CalendarEntityFeature {
|
||||
CREATE_EVENT = 1,
|
||||
DELETE_EVENT = 2,
|
||||
UPDATE_EVENT = 4,
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export type ClimateEntity = HassEntityBase & {
|
||||
};
|
||||
};
|
||||
|
||||
export const enum ClimateEntityFeature {
|
||||
export enum ClimateEntityFeature {
|
||||
TARGET_TEMPERATURE = 1,
|
||||
TARGET_TEMPERATURE_RANGE = 2,
|
||||
TARGET_HUMIDITY = 4,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum ConversationEntityFeature {
|
||||
export enum ConversationEntityFeature {
|
||||
CONTROL = 1,
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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 { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantFormatters } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum CoverEntityFeature {
|
||||
export enum CoverEntityFeature {
|
||||
OPEN = 1,
|
||||
CLOSE = 2,
|
||||
SET_POSITION = 4,
|
||||
@@ -122,7 +122,7 @@ export interface CoverEntity extends HassEntityBase {
|
||||
|
||||
export function computeCoverPositionStateDisplay(
|
||||
stateObj: CoverEntity,
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
position?: number
|
||||
) {
|
||||
const statePosition = stateActive(stateObj)
|
||||
@@ -133,7 +133,7 @@ export function computeCoverPositionStateDisplay(
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
||||
return currentPosition && currentPosition !== 100
|
||||
? hass.formatEntityAttributeValue(
|
||||
? formatEntityAttributeValue(
|
||||
stateObj,
|
||||
// Always use position as it's the same formatting as tilt position
|
||||
"current_position",
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
import type { HassEntityBase } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const stateToIsoDateString = (entityState: HassEntityBase) =>
|
||||
`${entityState}T00:00:00`;
|
||||
|
||||
export const setDateValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
date: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, date };
|
||||
hass.callService("date", "set_value", param);
|
||||
callService("date", "set_value", param);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const setDateTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
datetime: Date
|
||||
) => {
|
||||
hass.callService("datetime", "set_value", {
|
||||
callService("datetime", "set_value", {
|
||||
entity_id: entityId,
|
||||
datetime: datetime.toISOString(),
|
||||
});
|
||||
|
||||
@@ -277,7 +277,7 @@ export const getExtendedEntityRegistryEntries = (
|
||||
});
|
||||
|
||||
export const updateEntityRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entityId: string,
|
||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
): Promise<UpdateEntityRegistryEntryResult> =>
|
||||
|
||||
@@ -7,11 +7,12 @@ interface EntitySource {
|
||||
|
||||
export type EntitySources = Record<string, EntitySource>;
|
||||
|
||||
const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
|
||||
hass.callWS({ type: "entity/source" });
|
||||
const fetchEntitySources = (
|
||||
hass: Pick<HomeAssistant, "callWS">
|
||||
): Promise<EntitySources> => hass.callWS({ type: "entity/source" });
|
||||
|
||||
export const fetchEntitySourcesWithCache = (
|
||||
hass: HomeAssistant
|
||||
hass: Pick<HomeAssistant, "callWS" | "states">
|
||||
): Promise<EntitySources> =>
|
||||
timeCachePromiseFunc(
|
||||
"_entitySources",
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import type {
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum FanEntityFeature {
|
||||
export enum FanEntityFeature {
|
||||
SET_SPEED = 1,
|
||||
OSCILLATE = 2,
|
||||
DIRECTION = 4,
|
||||
|
||||
@@ -8,9 +8,13 @@ export const uploadFile = async (hass: HomeAssistant, file: File) => {
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error(`Uploaded file is too large (${file.name})`);
|
||||
throw new Error(
|
||||
hass.localize("ui.common.upload_file_too_large", {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
throw new Error(hass.localize("ui.common.unknown_error"));
|
||||
}
|
||||
const data = await resp.json();
|
||||
return data.file_id;
|
||||
|
||||
@@ -20,7 +20,7 @@ export type HumidifierEntity = HassEntityBase & {
|
||||
};
|
||||
};
|
||||
|
||||
export const enum HumidifierEntityFeature {
|
||||
export enum HumidifierEntityFeature {
|
||||
MODES = 1,
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -548,7 +548,9 @@ const getEntityIcon = async (
|
||||
};
|
||||
|
||||
export const attributeIcon = async (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: HomeAssistant["connection"],
|
||||
entities: HomeAssistant["entities"],
|
||||
state: HassEntity,
|
||||
attribute: string,
|
||||
attributeValue?: string
|
||||
@@ -556,7 +558,7 @@ export const attributeIcon = async (
|
||||
let icon: string | undefined;
|
||||
const domain = computeStateDomain(state);
|
||||
const deviceClass = state.attributes.device_class;
|
||||
const entity = hass.entities?.[state.entity_id] as
|
||||
const entity = entities[state.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
const platform = entity?.platform;
|
||||
@@ -567,8 +569,8 @@ export const attributeIcon = async (
|
||||
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
@@ -580,8 +582,8 @@ export const attributeIcon = async (
|
||||
}
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
hassConnection,
|
||||
hassConfig,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
|
||||
@@ -57,9 +57,13 @@ export const createImage = async (
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error(`Uploaded image is too large (${file.name})`);
|
||||
throw new Error(
|
||||
hass.localize("ui.common.upload_image_too_large", {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
throw new Error(hass.localize("ui.common.unknown_error"));
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputDateTime {
|
||||
id: string;
|
||||
@@ -32,13 +32,13 @@ export const stateToIsoDateString = (entityState: HassEntity) =>
|
||||
)}`;
|
||||
|
||||
export const setInputDateTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
time: string | undefined = undefined,
|
||||
date: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, time, date };
|
||||
hass.callService("input_datetime", "set_datetime", param);
|
||||
callService("input_datetime", "set_datetime", param);
|
||||
};
|
||||
|
||||
export const fetchInputDateTime = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -11,7 +11,7 @@ export type LawnMowerEntityState =
|
||||
| "docked"
|
||||
| "error";
|
||||
|
||||
export const enum LawnMowerEntityFeature {
|
||||
export enum LawnMowerEntityFeature {
|
||||
START_MOWING = 1,
|
||||
PAUSE = 2,
|
||||
DOCK = 4,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { temperature2rgb } from "../common/color/convert-light-color";
|
||||
|
||||
export const enum LightEntityFeature {
|
||||
export enum LightEntityFeature {
|
||||
EFFECT = 4,
|
||||
FLASH = 8,
|
||||
TRANSITION = 32,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
export enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
}
|
||||
|
||||
|
||||
+78
-195
@@ -1,15 +1,9 @@
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
BINARY_STATE_OFF,
|
||||
BINARY_STATE_ON,
|
||||
DOMAINS_WITH_DYNAMIC_PICTURE,
|
||||
} from "../common/const";
|
||||
import { 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";
|
||||
@@ -29,7 +23,7 @@ export interface LogbookEntry {
|
||||
message?: string;
|
||||
entity_id?: string;
|
||||
icon?: string;
|
||||
source?: string; // The trigger source
|
||||
source?: string; // The trigger source (English phrase, parsed for the cause)
|
||||
domain?: string;
|
||||
state?: string; // The state of the entity
|
||||
// Context data
|
||||
@@ -50,23 +44,27 @@ export interface LogbookEntry {
|
||||
// Localization mapping for all the triggers in core
|
||||
// in homeassistant.components.homeassistant.triggers
|
||||
//
|
||||
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";
|
||||
// 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";
|
||||
|
||||
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
|
||||
// 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
|
||||
};
|
||||
|
||||
export const getLogbookDataForContext = async (
|
||||
@@ -158,215 +156,100 @@ export const createHistoricState = (
|
||||
state: state,
|
||||
attributes: {
|
||||
// Rebuild the historical state by copying static attributes only
|
||||
device_class: currentStateObj?.attributes.device_class,
|
||||
source_type: currentStateObj?.attributes.source_type,
|
||||
has_date: currentStateObj?.attributes.has_date,
|
||||
has_time: currentStateObj?.attributes.has_time,
|
||||
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,
|
||||
// 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 triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
|
||||
const phrase = triggerPhrases[key];
|
||||
if (source.startsWith(phrase)) {
|
||||
return source.replace(
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
|
||||
);
|
||||
return source.replace(phrase, localize(`ui.components.logbook.${key}`));
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
// 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"
|
||||
export type TriggerPlatform =
|
||||
| "state"
|
||||
| "numeric_state"
|
||||
| "time"
|
||||
| "time_pattern"
|
||||
| "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",
|
||||
| "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",
|
||||
};
|
||||
|
||||
// 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}`)}`
|
||||
);
|
||||
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;
|
||||
}
|
||||
const rest = source.slice(phrase.length).trim();
|
||||
const entityId = /^[a-z_]+\.[a-z0-9_]+$/.test(rest) ? rest : undefined;
|
||||
return { platform: triggerPlatform[key], entityId };
|
||||
}
|
||||
return source;
|
||||
return {};
|
||||
};
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
state: string,
|
||||
stateObj: HassEntity,
|
||||
domain: string
|
||||
): string => {
|
||||
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;
|
||||
// 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`);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
// 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);
|
||||
};
|
||||
|
||||
export const filterLogbookCompatibleEntities = (entity) => {
|
||||
|
||||
@@ -82,7 +82,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
|
||||
| "buffering";
|
||||
}
|
||||
|
||||
export const enum MediaPlayerEntityFeature {
|
||||
export enum MediaPlayerEntityFeature {
|
||||
PAUSE = 1,
|
||||
SEEK = 2,
|
||||
VOLUME_SET = 4,
|
||||
|
||||
@@ -54,9 +54,13 @@ export const uploadLocalMedia = async (
|
||||
}
|
||||
);
|
||||
if (resp.status === 413) {
|
||||
throw new Error(`Uploaded file is too large (${file.name})`);
|
||||
throw new Error(
|
||||
hass.localize("ui.common.upload_file_too_large", {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
throw new Error(hass.localize("ui.common.unknown_error"));
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
|
||||
@@ -153,21 +153,6 @@ export const getRecorderInfo = (conn: Connection) =>
|
||||
type: "recorder/info",
|
||||
});
|
||||
|
||||
export type EntityRecordingDisabler = "user";
|
||||
|
||||
export interface RecordedEntityOptions {
|
||||
recording_disabled_by: EntityRecordingDisabler | null;
|
||||
}
|
||||
|
||||
export const getRecordedEntity = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<RecordedEntityOptions>({
|
||||
type: "recorder/recorded_entities/get",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const getStatisticIds = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
statistic_type?: "mean" | "sum"
|
||||
|
||||
+28
-7
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { Describe } from "superstruct";
|
||||
@@ -104,6 +105,9 @@ export interface Field {
|
||||
selector?: any;
|
||||
}
|
||||
|
||||
const getScriptFields = (services: HassServices, entityId: string) =>
|
||||
services.script[computeObjectId(entityId)]?.fields;
|
||||
|
||||
interface BaseAction {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
@@ -391,31 +395,41 @@ export const getActionType = (action: Action): ActionType => {
|
||||
export const isAction = (value: unknown): value is Action =>
|
||||
getActionType(value as Action) !== "unknown";
|
||||
|
||||
export const hasScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
export const hasScriptFieldsForServices = (
|
||||
services: HassServices,
|
||||
entityId: string
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
return fields !== undefined && Object.keys(fields).length > 0;
|
||||
};
|
||||
|
||||
export const hasRequiredScriptFields = (
|
||||
export const hasScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean => hasScriptFieldsForServices(hass.services, entityId);
|
||||
|
||||
export const hasRequiredScriptFieldsForServices = (
|
||||
services: HassServices,
|
||||
entityId: string
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
return (
|
||||
fields !== undefined &&
|
||||
Object.values(fields).some((field) => field.required)
|
||||
);
|
||||
};
|
||||
|
||||
export const requiredScriptFieldsFilled = (
|
||||
export const hasRequiredScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean => hasRequiredScriptFieldsForServices(hass.services, entityId);
|
||||
|
||||
export const requiredScriptFieldsFilledForServices = (
|
||||
services: HassServices,
|
||||
entityId: string,
|
||||
data?: Record<string, any>
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
if (fields === undefined || Object.keys(fields).length === 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -430,6 +444,13 @@ export const requiredScriptFieldsFilled = (
|
||||
});
|
||||
};
|
||||
|
||||
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[] => {
|
||||
|
||||
@@ -82,6 +82,7 @@ export type Selector =
|
||||
| UiActionSelector
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector
|
||||
| UiTimeFormatSelector
|
||||
| BackupLocationSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
@@ -601,6 +602,10 @@ export interface UiStateContentSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiTimeFormatSelector {
|
||||
ui_time_format: {} | null;
|
||||
}
|
||||
|
||||
export interface EntityNameSelector {
|
||||
entity_name: {
|
||||
entity_id?: string;
|
||||
|
||||
@@ -6,7 +6,9 @@ export interface SupervisorUpdateConfig {
|
||||
core_backup_before_update: boolean;
|
||||
}
|
||||
|
||||
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
|
||||
export const getSupervisorUpdateConfig = async (
|
||||
hass: Pick<HomeAssistant, "callWS">
|
||||
) =>
|
||||
hass.callWS<SupervisorUpdateConfig>({
|
||||
type: "hassio/update/config/info",
|
||||
});
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const setTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
time: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, time: time };
|
||||
hass.callService("time", "set_value", param);
|
||||
callService("time", "set_value", param);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,10 @@ 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 & {
|
||||
@@ -100,3 +102,11 @@ 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
@@ -31,7 +31,7 @@ export interface TodoItem {
|
||||
completed?: string | null;
|
||||
}
|
||||
|
||||
export const enum TodoListEntityFeature {
|
||||
export enum TodoListEntityFeature {
|
||||
CREATE_TODO_ITEM = 1,
|
||||
DELETE_TODO_ITEM = 2,
|
||||
UPDATE_TODO_ITEM = 4,
|
||||
|
||||
+4
-1
@@ -77,7 +77,10 @@ export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
!!entity.attributes.in_progress;
|
||||
|
||||
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
export const updateReleaseNotes = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entityId: string
|
||||
) =>
|
||||
hass.callWS<string | null>({
|
||||
type: "update/release_notes",
|
||||
entity_id: entityId,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export type VacuumEntityState =
|
||||
| "returning"
|
||||
| "error";
|
||||
|
||||
export const enum VacuumEntityFeature {
|
||||
export enum VacuumEntityFeature {
|
||||
TURN_ON = 1,
|
||||
TURN_OFF = 2,
|
||||
PAUSE = 4,
|
||||
|
||||
+4
-4
@@ -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 { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantFormatters } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum ValveEntityFeature {
|
||||
export enum ValveEntityFeature {
|
||||
OPEN = 1,
|
||||
CLOSE = 2,
|
||||
SET_POSITION = 4,
|
||||
@@ -78,7 +78,7 @@ export interface ValveEntity extends HassEntityBase {
|
||||
|
||||
export function computeValvePositionStateDisplay(
|
||||
stateObj: ValveEntity,
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
position?: number
|
||||
) {
|
||||
const statePosition = stateActive(stateObj)
|
||||
@@ -88,7 +88,7 @@ export function computeValvePositionStateDisplay(
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
||||
return currentPosition && currentPosition !== 100
|
||||
? hass.formatEntityAttributeValue(
|
||||
? formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"current_position",
|
||||
Math.round(currentPosition)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const enum WaterHeaterEntityFeature {
|
||||
export enum WaterHeaterEntityFeature {
|
||||
TARGET_TEMPERATURE = 1,
|
||||
OPERATION_MODE = 2,
|
||||
AWAY_MODE = 4,
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ import { round } from "../common/number/round";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum WeatherEntityFeature {
|
||||
export enum WeatherEntityFeature {
|
||||
FORECAST_DAILY = 1,
|
||||
FORECAST_HOURLY = 2,
|
||||
FORECAST_TWICE_DAILY = 4,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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,
|
||||
@@ -20,7 +25,11 @@ import type {
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
} from "../../../../types";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
@@ -46,7 +55,20 @@ const favoriteKindFromEvent = (ev: Event): FavoriteKind =>
|
||||
|
||||
@customElement("ha-more-info-cover-favorite-positions")
|
||||
export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@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 stateObj!: CoverEntity;
|
||||
|
||||
@@ -85,7 +107,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
key: FavoriteLocalizeKey,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
`ui.dialogs.more_info_control.cover.${kind === "position" ? "favorite_position" : "favorite_tilt_position"}.${key}`,
|
||||
values
|
||||
);
|
||||
@@ -124,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "cover",
|
||||
@@ -169,14 +191,14 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
}
|
||||
|
||||
if (kind === "position") {
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
this._api.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: favorite,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
this._api.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
tilt_position: favorite,
|
||||
});
|
||||
@@ -191,7 +213,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
kind,
|
||||
value === undefined ? "add_title" : "edit_title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
inputLabel: this._localize(
|
||||
kind === "position"
|
||||
? "ui.card.cover.position"
|
||||
: "ui.card.cover.tilt_position"
|
||||
@@ -311,7 +333,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
if (action === "hold" && this._user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
@@ -376,10 +398,10 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
.deleteLabel=${this._deleteLabel(kind)}
|
||||
.editMode=${this.editMode ?? false}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.isAdmin=${Boolean(this._user?.is_admin)}
|
||||
.showDone=${showDone}
|
||||
.addLabel=${this._localizeFavorite(kind, "add")}
|
||||
.doneLabel=${this.hass.localize(
|
||||
.doneLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
@@ -415,7 +437,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
${supportsPosition
|
||||
? this._renderKindSection(
|
||||
"position",
|
||||
this.hass.localize("ui.card.cover.position"),
|
||||
this._localize("ui.card.cover.position"),
|
||||
this._favoritePositions,
|
||||
showDoneOnPosition,
|
||||
showLabels
|
||||
@@ -424,7 +446,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
${supportsTiltPosition
|
||||
? this._renderKindSection(
|
||||
"tilt",
|
||||
this.hass.localize("ui.card.cover.tilt_position"),
|
||||
this._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,6 +21,10 @@ 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 &&
|
||||
@@ -29,7 +33,6 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(this.stateObj.state)}
|
||||
format="relative"
|
||||
capitalize
|
||||
@@ -37,7 +40,7 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
return this.hass.formatEntityState(this.stateObj);
|
||||
return this._formatters?.formatEntityState(this.stateObj) ?? "";
|
||||
}
|
||||
|
||||
private _toggleAbsolute() {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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 {
|
||||
@@ -13,7 +18,11 @@ import type {
|
||||
ValveEntityOptions,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
} from "../../../../types";
|
||||
import type { ValveEntity } from "../../../../data/valve";
|
||||
import { DEFAULT_VALVE_FAVORITE_POSITIONS } from "../../../../data/valve";
|
||||
import { normalizeFavoritePositions } from "../../../../data/favorite_positions";
|
||||
@@ -37,7 +46,20 @@ type FavoriteLocalizeKey =
|
||||
|
||||
@customElement("ha-more-info-valve-favorite-positions")
|
||||
export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@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 stateObj!: ValveEntity;
|
||||
|
||||
@@ -64,7 +86,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
key: FavoriteLocalizeKey,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
`ui.dialogs.more_info_control.valve.favorite_position.${key}`,
|
||||
values
|
||||
);
|
||||
@@ -88,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
currentOptions.favorite_positions = this._favoritePositions;
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "valve",
|
||||
@@ -122,7 +144,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("valve", "set_valve_position", {
|
||||
this._api.callService("valve", "set_valve_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: favorite,
|
||||
});
|
||||
@@ -135,7 +157,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
title: this._localizeFavorite(
|
||||
value === undefined ? "add_title" : "edit_title"
|
||||
),
|
||||
inputLabel: this.hass.localize("ui.card.valve.position"),
|
||||
inputLabel: this._localize("ui.card.valve.position"),
|
||||
inputType: "number",
|
||||
inputMin: "0",
|
||||
inputMax: "100",
|
||||
@@ -242,7 +264,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
if (action === "hold" && this._user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
@@ -296,10 +318,10 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
.deleteLabel=${this._deleteLabel}
|
||||
.editMode=${this.editMode ?? false}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.isAdmin=${Boolean(this._user?.is_admin)}
|
||||
.showDone=${true}
|
||||
.addLabel=${this._localizeFavorite("add")}
|
||||
.doneLabel=${this.hass.localize(
|
||||
.doneLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-relative-time";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import { triggerAutomationActions } from "../../../data/automation";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistantApi } 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.hass || !this.stateObj) {
|
||||
if (!this._localize || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hr />
|
||||
<div class="flex">
|
||||
<div>${this.hass.localize("ui.card.automation.last_triggered")}:</div>
|
||||
<div>${this._localize("ui.card.automation.last_triggered")}:</div>
|
||||
<ha-relative-time
|
||||
.datetime=${this.stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
@@ -35,14 +45,14 @@ class MoreInfoAutomation extends LitElement {
|
||||
@click=${this._runActions}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
${this.hass.localize("ui.card.automation.trigger")}
|
||||
${this._localize("ui.card.automation.trigger")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _runActions() {
|
||||
triggerAutomationActions(this.hass, this.stateObj!.entity_id);
|
||||
triggerAutomationActions(this._api, this.stateObj!.entity_id);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiArrowOscillating,
|
||||
mdiFan,
|
||||
@@ -8,7 +10,9 @@ 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";
|
||||
@@ -22,10 +26,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";
|
||||
@@ -34,15 +38,24 @@ 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}
|
||||
@@ -50,7 +63,6 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
private _renderFanModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="fan_mode"
|
||||
.attributeValue=${value}
|
||||
@@ -58,7 +70,6 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
private _renderSwingModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="swing_mode"
|
||||
.attributeValue=${value}
|
||||
@@ -66,7 +77,6 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
private _renderSwingHorizontalModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="swing_horizontal_mode"
|
||||
.attributeValue=${value}
|
||||
@@ -120,13 +130,13 @@ class MoreInfoClimate extends LitElement {
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
${this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}
|
||||
@@ -138,13 +148,13 @@ class MoreInfoClimate extends LitElement {
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
${this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}
|
||||
@@ -157,7 +167,6 @@ class MoreInfoClimate extends LitElement {
|
||||
${this._mainControl === "temperature"
|
||||
? html`
|
||||
<ha-state-control-climate-temperature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-control-climate-temperature>
|
||||
`
|
||||
@@ -165,7 +174,6 @@ class MoreInfoClimate extends LitElement {
|
||||
${this._mainControl === "humidity"
|
||||
? html`
|
||||
<ha-state-control-climate-humidity
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-control-climate-humidity>
|
||||
`
|
||||
@@ -176,7 +184,7 @@ class MoreInfoClimate extends LitElement {
|
||||
<ha-icon-button-toggle
|
||||
.selected=${this._mainControl === "temperature"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.more_info_control.climate.temperature"
|
||||
)}
|
||||
.control=${"temperature"}
|
||||
@@ -187,7 +195,7 @@ class MoreInfoClimate extends LitElement {
|
||||
<ha-icon-button-toggle
|
||||
.selected=${this._mainControl === "humidity"}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.more_info_control.climate.humidity"
|
||||
)}
|
||||
.control=${"humidity"}
|
||||
@@ -201,8 +209,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
<ha-more-info-control-select-container>
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.card.climate.mode")}
|
||||
.label=${this._localize("ui.card.climate.mode")}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.options=${stateObj.attributes.hvac_modes
|
||||
@@ -211,7 +218,7 @@ class MoreInfoClimate extends LitElement {
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
iconPath: climateHvacModeIcon(mode),
|
||||
label: this.hass.formatEntityState(stateObj, mode),
|
||||
label: this._formatters.formatEntityState(stateObj, mode),
|
||||
}))}
|
||||
@wa-select=${this._handleOperationModeChanged}
|
||||
>
|
||||
@@ -223,8 +230,7 @@ class MoreInfoClimate extends LitElement {
|
||||
${supportPresetMode && stateObj.attributes.preset_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"preset_mode"
|
||||
)}
|
||||
@@ -233,7 +239,7 @@ class MoreInfoClimate extends LitElement {
|
||||
@wa-select=${this._handlePresetmodeChanged}
|
||||
.options=${stateObj.attributes.preset_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"preset_mode",
|
||||
mode
|
||||
@@ -248,8 +254,7 @@ class MoreInfoClimate extends LitElement {
|
||||
${supportFanMode && stateObj.attributes.fan_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"fan_mode"
|
||||
)}
|
||||
@@ -258,7 +263,7 @@ class MoreInfoClimate extends LitElement {
|
||||
@wa-select=${this._handleFanModeChanged}
|
||||
.options=${stateObj.attributes.fan_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_mode",
|
||||
mode
|
||||
@@ -273,8 +278,7 @@ class MoreInfoClimate extends LitElement {
|
||||
${supportSwingMode && stateObj.attributes.swing_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"swing_mode"
|
||||
)}
|
||||
@@ -283,7 +287,7 @@ class MoreInfoClimate extends LitElement {
|
||||
@wa-select=${this._handleSwingmodeChanged}
|
||||
.options=${stateObj.attributes.swing_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_mode",
|
||||
mode
|
||||
@@ -302,8 +306,7 @@ class MoreInfoClimate extends LitElement {
|
||||
stateObj.attributes.swing_horizontal_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"swing_horizontal_mode"
|
||||
)}
|
||||
@@ -313,7 +316,7 @@ class MoreInfoClimate extends LitElement {
|
||||
.options=${stateObj.attributes.swing_horizontal_modes.map(
|
||||
(mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_horizontal_mode",
|
||||
mode
|
||||
@@ -403,7 +406,7 @@ class MoreInfoClimate extends LitElement {
|
||||
data.entity_id = this.stateObj!.entity_id;
|
||||
const curState = this.stateObj;
|
||||
|
||||
await this.hass.callService("climate", service, data);
|
||||
await this._api.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,3 +1,4 @@
|
||||
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";
|
||||
@@ -5,17 +6,20 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import type { HomeAssistantApi } 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;
|
||||
|
||||
private _fieldInput = {};
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
private _fieldInput: Record<string, unknown> = {};
|
||||
|
||||
protected render() {
|
||||
if (this.stateObj?.state !== "configure") {
|
||||
@@ -71,7 +75,7 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
|
||||
this._isConfiguring = true;
|
||||
|
||||
this.hass.callService("configurator", "configure", data).then(
|
||||
this._api.callService("configurator", "configure", data).then(
|
||||
() => {
|
||||
this._isConfiguring = false;
|
||||
},
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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 type { HomeAssistant } from "../../../types";
|
||||
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 "../../../components/ha-assist-chat";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -11,14 +15,20 @@ 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);
|
||||
|
||||
@@ -40,7 +50,7 @@ class MoreInfoConversation extends LitElement {
|
||||
this._errorLoadAssist = undefined;
|
||||
const pipelineId = this.stateObj!.entity_id;
|
||||
try {
|
||||
const pipeline = await getAssistPipeline(this.hass, pipelineId);
|
||||
const pipeline = await getAssistPipeline(this._api, pipelineId);
|
||||
// Verify the pipeline is still the same.
|
||||
if (this.stateObj && pipelineId === this.stateObj.entity_id) {
|
||||
this._pipeline = pipeline;
|
||||
@@ -61,21 +71,20 @@ class MoreInfoConversation extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._localize || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._errorLoadAssist
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
${this._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,18 +1,28 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-button";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistantApi } 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.hass || !this.stateObj) {
|
||||
if (!this._localize || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -28,7 +38,7 @@ class MoreInfoCounter extends LitElement {
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.increment")}
|
||||
${this._localize("ui.card.counter.actions.increment")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -38,7 +48,7 @@ class MoreInfoCounter extends LitElement {
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.decrement")}
|
||||
${this._localize("ui.card.counter.actions.decrement")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -47,7 +57,7 @@ class MoreInfoCounter extends LitElement {
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.reset")}
|
||||
${this._localize("ui.card.counter.actions.reset")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
@@ -55,7 +65,7 @@ class MoreInfoCounter extends LitElement {
|
||||
|
||||
private _handleActionClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget as any).action;
|
||||
this.hass.callService("counter", action, {
|
||||
this._api.callService("counter", action, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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,
|
||||
@@ -21,7 +25,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 { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistantFormatters } 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";
|
||||
@@ -30,7 +34,13 @@ type Mode = "position" | "button";
|
||||
|
||||
@customElement("more-info-cover")
|
||||
class MoreInfoCover extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CoverEntity;
|
||||
|
||||
@@ -58,11 +68,11 @@ class MoreInfoCover extends LitElement {
|
||||
}
|
||||
|
||||
private get _stateOverride() {
|
||||
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
|
||||
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
|
||||
|
||||
const positionStateDisplay = computeCoverPositionStateDisplay(
|
||||
this.stateObj!,
|
||||
this.hass
|
||||
this._formatters.formatEntityAttributeValue
|
||||
);
|
||||
|
||||
if (positionStateDisplay) {
|
||||
@@ -72,7 +82,7 @@ class MoreInfoCover extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -113,7 +123,6 @@ 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>
|
||||
@@ -126,7 +135,6 @@ class MoreInfoCover extends LitElement {
|
||||
? html`
|
||||
<ha-state-control-cover-position
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-control-cover-position>
|
||||
`
|
||||
: nothing}
|
||||
@@ -134,7 +142,6 @@ class MoreInfoCover extends LitElement {
|
||||
? html`
|
||||
<ha-state-control-cover-tilt-position
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-control-cover-tilt-position>
|
||||
`
|
||||
: nothing}
|
||||
@@ -148,14 +155,12 @@ 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}
|
||||
@@ -169,7 +174,7 @@ class MoreInfoCover extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button-group>
|
||||
<ha-icon-button-toggle
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
`ui.dialogs.more_info_control.cover.switch_mode.position`
|
||||
)}
|
||||
.selected=${this._mode === "position"}
|
||||
@@ -178,7 +183,7 @@ class MoreInfoCover extends LitElement {
|
||||
@click=${this._setMode}
|
||||
></ha-icon-button-toggle>
|
||||
<ha-icon-button-toggle
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
`ui.dialogs.more_info_control.cover.switch_mode.button`
|
||||
)}
|
||||
.selected=${this._mode === "button"}
|
||||
@@ -195,7 +200,6 @@ 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,18 +1,35 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
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 { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantInternationalization,
|
||||
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;
|
||||
@@ -20,7 +37,7 @@ class MoreInfoDate extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.value=${this.stateObj.state === UNKNOWN
|
||||
? undefined
|
||||
: this.stateObj.state}
|
||||
@@ -32,7 +49,11 @@ class MoreInfoDate extends LitElement {
|
||||
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
setDateValue(
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
ev.detail.value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
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 { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantInternationalization,
|
||||
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;
|
||||
@@ -27,14 +44,14 @@ class MoreInfoDatetime extends LitElement {
|
||||
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
|
||||
|
||||
return html`<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.value=${date}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
.value=${time}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>`;
|
||||
@@ -50,7 +67,11 @@ class MoreInfoDatetime extends LitElement {
|
||||
const newTime = ev.detail.value.split(":").map(Number);
|
||||
dateObj.setHours(newTime[0], newTime[1], newTime[2]);
|
||||
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
setDateTimeValue(
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
dateObj
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +81,11 @@ class MoreInfoDatetime extends LitElement {
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]);
|
||||
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
setDateTimeValue(
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
dateObj
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ class MoreInfoFan extends LitElement {
|
||||
|
||||
private _renderPresetModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${value}
|
||||
@@ -50,7 +49,6 @@ class MoreInfoFan extends LitElement {
|
||||
|
||||
private _renderDirectionIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="direction"
|
||||
.attributeValue=${value}
|
||||
@@ -241,7 +239,6 @@ class MoreInfoFan extends LitElement {
|
||||
>
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="direction"
|
||||
.attributeValue=${this.stateObj.attributes.direction}
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
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}
|
||||
@@ -43,7 +56,6 @@ class MoreInfoHumidifier extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hass = this.hass;
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
const supportModes = supportsFeature(
|
||||
@@ -57,13 +69,13 @@ class MoreInfoHumidifier extends LitElement {
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
${this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity"
|
||||
)}
|
||||
@@ -75,22 +87,20 @@ 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
|
||||
.hass=${hass}
|
||||
.label=${this.hass.localize("ui.card.humidifier.state")}
|
||||
.label=${this._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.hass.formatEntityState(this.stateObj, fanState)
|
||||
? this._formatters.formatEntityState(this.stateObj, fanState)
|
||||
: fanState,
|
||||
}))}
|
||||
>
|
||||
@@ -100,15 +110,14 @@ class MoreInfoHumidifier extends LitElement {
|
||||
${supportModes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${hass}
|
||||
.label=${hass.localize("ui.card.humidifier.mode")}
|
||||
.label=${this._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.hass.formatEntityAttributeValue(
|
||||
? this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"mode",
|
||||
mode
|
||||
@@ -164,7 +173,7 @@ class MoreInfoHumidifier extends LitElement {
|
||||
data.entity_id = this.stateObj!.entity_id;
|
||||
const curState = this.stateObj;
|
||||
|
||||
await this.hass.callService("humidifier", service, data);
|
||||
await this._api.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,32 +2,31 @@ 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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
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 {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._localize || !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,21 +1,38 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
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 { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantInternationalization,
|
||||
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;
|
||||
@@ -25,7 +42,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
${this.stateObj.attributes.has_date
|
||||
? html`
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.value=${stateToIsoDateString(this.stateObj)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._dateChanged}
|
||||
@@ -41,7 +58,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
: this.stateObj.attributes.has_date
|
||||
? this.stateObj.state.split(" ")[1]
|
||||
: this.stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
@@ -57,7 +74,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
ev.detail.value,
|
||||
this.stateObj!.attributes.has_date
|
||||
@@ -68,7 +85,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
this.stateObj!.attributes.has_time
|
||||
? this.stateObj!.state.split(" ")[1]
|
||||
|
||||
@@ -60,7 +60,6 @@ class MoreInfoLight extends LitElement {
|
||||
|
||||
private _renderEffectIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="effect"
|
||||
.attributeValue=${value}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import 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 { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistantApi, HomeAssistantFormatters } 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.hass || !this.stateObj) {
|
||||
if (!this._localize || !this._formatters || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -24,14 +38,14 @@ class MoreInfoRemote extends LitElement {
|
||||
${supportsFeature(stateObj, REMOTE_SUPPORT_ACTIVITY)
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass!.localize(
|
||||
.label=${this._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.hass!.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"activity",
|
||||
activity
|
||||
@@ -52,7 +66,7 @@ class MoreInfoRemote extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("remote", "turn_on", {
|
||||
this._api.callService("remote", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
activity: newVal,
|
||||
});
|
||||
|
||||
@@ -2,10 +2,11 @@ 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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
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";
|
||||
@@ -14,12 +15,12 @@ import { showSirenAdvancedControlsView } from "../components/siren/show-dialog-s
|
||||
|
||||
@customElement("more-info-siren")
|
||||
class MoreInfoSiren extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._localize || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -39,13 +40,11 @@ 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>
|
||||
@@ -55,7 +54,7 @@ class MoreInfoSiren extends LitElement {
|
||||
size="s"
|
||||
@click=${this._showAdvancedControlsDialog}
|
||||
>
|
||||
${this.hass.localize("ui.components.siren.advanced_controls")}
|
||||
${this._localize("ui.components.siren.advanced_controls")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -2,32 +2,31 @@ 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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
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 {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this._localize || !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,18 +1,35 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
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 { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantInternationalization,
|
||||
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;
|
||||
@@ -23,7 +40,7 @@ class MoreInfoTime extends LitElement {
|
||||
.value=${this.stateObj.state === UNKNOWN
|
||||
? undefined
|
||||
: this.stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>
|
||||
@@ -36,7 +53,11 @@ class MoreInfoTime extends LitElement {
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
setTimeValue(
|
||||
this._api.callService,
|
||||
this.stateObj!.entity_id,
|
||||
ev.detail.value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,69 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-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 type { HomeAssistant } from "../../../types";
|
||||
import { timerDurationData } from "../../../data/timer";
|
||||
import type { HomeAssistantApi, ValueChangedEvent } 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.hass || !this.stateObj) {
|
||||
if (!this._localize || !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">
|
||||
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
|
||||
${timerState === "idle"
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
.action=${"start"}
|
||||
@click=${this._handleActionClick}
|
||||
>
|
||||
${this.hass!.localize("ui.card.timer.actions.start")}
|
||||
<ha-button appearance="plain" size="s" @click=${this._start}>
|
||||
${this._localize("ui.card.timer.actions.start")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
${this.stateObj.state === "active"
|
||||
: 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"
|
||||
@@ -37,11 +71,23 @@ class MoreInfoTimer extends LitElement {
|
||||
.action=${"pause"}
|
||||
@click=${this._handleActionClick}
|
||||
>
|
||||
${this.hass!.localize("ui.card.timer.actions.pause")}
|
||||
${this._localize("ui.card.timer.actions.pause")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
${this.stateObj.state === "active" || this.stateObj.state === "paused"
|
||||
: nothing}
|
||||
${timerState === "paused"
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
.action=${"start"}
|
||||
@click=${this._handleActionClick}
|
||||
>
|
||||
${this._localize("ui.card.timer.actions.start")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${timerState === "active" || timerState === "paused"
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -49,7 +95,7 @@ class MoreInfoTimer extends LitElement {
|
||||
.action=${"cancel"}
|
||||
@click=${this._handleActionClick}
|
||||
>
|
||||
${this.hass!.localize("ui.card.timer.actions.cancel")}
|
||||
${this._localize("ui.card.timer.actions.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -57,26 +103,48 @@ class MoreInfoTimer extends LitElement {
|
||||
.action=${"finish"}
|
||||
@click=${this._handleActionClick}
|
||||
>
|
||||
${this.hass!.localize("ui.card.timer.actions.finish")}
|
||||
${this._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.hass.callService("timer", action, {
|
||||
this._api.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,9 +1,14 @@
|
||||
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";
|
||||
@@ -15,10 +20,18 @@ 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,
|
||||
@@ -28,15 +41,49 @@ import {
|
||||
updateIsInstalling,
|
||||
updateReleaseNotes,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} 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;
|
||||
@@ -51,7 +98,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
try {
|
||||
const { config } = await fetchBackupConfig(this.hass);
|
||||
const { config } = await fetchBackupConfig(this._api);
|
||||
this._backupConfig = config;
|
||||
} catch (err) {
|
||||
// ignore error, because user will get a manual backup option
|
||||
@@ -62,7 +109,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
private async _fetchUpdateBackupConfig(type: UpdateType) {
|
||||
try {
|
||||
const config = await getSupervisorUpdateConfig(this.hass);
|
||||
const config = await getSupervisorUpdateConfig(this._api);
|
||||
|
||||
// for home assistant and OS updates
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
@@ -81,7 +128,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
this._entitySources = await fetchEntitySourcesWithCache({
|
||||
callWS: this._api.callWS,
|
||||
states: this._states,
|
||||
});
|
||||
}
|
||||
|
||||
private _isHaOrOsUpdate(type: UpdateType): boolean {
|
||||
@@ -111,10 +161,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
if (!isBackupConfigValid) {
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual"
|
||||
),
|
||||
description: this.hass.localize(
|
||||
description: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual_description"
|
||||
),
|
||||
};
|
||||
@@ -127,22 +177,22 @@ class MoreInfoUpdate extends LitElement {
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic"
|
||||
),
|
||||
description: lastAutomaticBackupDate
|
||||
? this.hass.localize(
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
|
||||
{
|
||||
relative_time: relativeTime(
|
||||
lastAutomaticBackupDate,
|
||||
this.hass.locale,
|
||||
this._locale,
|
||||
now,
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
|
||||
),
|
||||
};
|
||||
@@ -152,11 +202,11 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (updateType === "addon") {
|
||||
const version = this.stateObj.attributes.installed_version;
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app"
|
||||
),
|
||||
description: version
|
||||
? this.hass.localize(
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app_description",
|
||||
{ version: version }
|
||||
)
|
||||
@@ -166,7 +216,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
// Fallback to generic UI
|
||||
return {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.generic"
|
||||
),
|
||||
};
|
||||
@@ -174,7 +224,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this._localize ||
|
||||
!this.stateObj ||
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
@@ -202,26 +252,26 @@ class MoreInfoUpdate extends LitElement {
|
||||
: nothing}
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"installed_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.installed_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
this._localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
this._localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -233,7 +283,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
@@ -300,7 +350,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this._handleClearSkipped}
|
||||
>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.more_info_control.update.clear_skipped"
|
||||
)}
|
||||
</ha-button>
|
||||
@@ -313,9 +363,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
${this._localize("ui.dialogs.more_info_control.update.skip")}
|
||||
</ha-button>
|
||||
`}
|
||||
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
|
||||
@@ -325,7 +373,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
.loading=${updateIsInstalling(this.stateObj)}
|
||||
.disabled=${updateButtonIsDisabled(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.more_info_control.update.update"
|
||||
)}
|
||||
</ha-button>
|
||||
@@ -352,7 +400,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
this._fetchEntitySources().then(() => {
|
||||
const type = getUpdateType(this.stateObj!, this._entitySources!);
|
||||
if (
|
||||
isComponentLoaded(this.hass.config, "hassio") &&
|
||||
isComponentLoaded(this._config, "hassio") &&
|
||||
["addon", "home_assistant", "home_assistant_os"].includes(type)
|
||||
) {
|
||||
this._fetchUpdateBackupConfig(type);
|
||||
@@ -374,7 +422,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
private async _fetchReleaseNotes() {
|
||||
try {
|
||||
this._releaseNotes = await updateReleaseNotes(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
} catch (err: any) {
|
||||
@@ -405,7 +453,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
installData.version = this.stateObj!.attributes.latest_version;
|
||||
}
|
||||
|
||||
this.hass.callService("update", "install", installData);
|
||||
this._api.callService("update", "install", installData);
|
||||
}
|
||||
|
||||
private _createBackupChanged(ev) {
|
||||
@@ -415,22 +463,22 @@ class MoreInfoUpdate extends LitElement {
|
||||
private _handleSkip(): void {
|
||||
if (this.stateObj!.attributes.auto_update) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
text: this._localize(
|
||||
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callService("update", "skip", {
|
||||
this._api.callService("update", "skip", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearSkipped(): void {
|
||||
this.hass.callService("update", "clear_skipped", {
|
||||
this._api.callService("update", "clear_skipped", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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,
|
||||
@@ -18,7 +22,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 { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistantFormatters } 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";
|
||||
@@ -27,7 +31,13 @@ type Mode = "position" | "button";
|
||||
|
||||
@customElement("more-info-valve")
|
||||
class MoreInfoValve extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: ValveEntity;
|
||||
|
||||
@@ -55,11 +65,11 @@ class MoreInfoValve extends LitElement {
|
||||
}
|
||||
|
||||
private get _stateOverride() {
|
||||
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
|
||||
const stateDisplay = this._formatters.formatEntityState(this.stateObj!);
|
||||
|
||||
const positionStateDisplay = computeValvePositionStateDisplay(
|
||||
this.stateObj!,
|
||||
this.hass
|
||||
this._formatters.formatEntityAttributeValue
|
||||
);
|
||||
|
||||
if (positionStateDisplay) {
|
||||
@@ -69,7 +79,7 @@ class MoreInfoValve extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -97,7 +107,6 @@ 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>
|
||||
@@ -110,7 +119,6 @@ class MoreInfoValve extends LitElement {
|
||||
? html`
|
||||
<ha-state-control-valve-position
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-control-valve-position>
|
||||
`
|
||||
: nothing}
|
||||
@@ -124,14 +132,12 @@ 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}
|
||||
@@ -144,7 +150,7 @@ class MoreInfoValve extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button-group>
|
||||
<ha-icon-button-toggle
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
`ui.dialogs.more_info_control.valve.switch_mode.position`
|
||||
)}
|
||||
.selected=${this._mode === "position"}
|
||||
@@ -153,7 +159,7 @@ class MoreInfoValve extends LitElement {
|
||||
@click=${this._setMode}
|
||||
></ha-icon-button-toggle>
|
||||
<ha-icon-button-toggle
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
`ui.dialogs.more_info_control.valve.switch_mode.button`
|
||||
)}
|
||||
.selected=${this._mode === "button"}
|
||||
@@ -170,7 +176,6 @@ 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,8 +1,12 @@
|
||||
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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -13,20 +17,29 @@ 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}
|
||||
@@ -57,13 +70,13 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
? html`
|
||||
<div>
|
||||
<p class="label">
|
||||
${this.hass.formatEntityAttributeName(
|
||||
${this._formatters.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}
|
||||
</p>
|
||||
<p class="value">
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
${this._formatters.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature"
|
||||
)}
|
||||
@@ -74,7 +87,6 @@ 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>
|
||||
@@ -82,8 +94,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
${supportOperationMode && stateObj.attributes.operation_list
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.card.water_heater.mode")}
|
||||
.label=${this._localize("ui.card.water_heater.mode")}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._handleOperationModeChanged}
|
||||
@@ -92,7 +103,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
.sort(compareWaterHeaterOperationMode)
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityState(stateObj, mode),
|
||||
label: this._formatters.formatEntityState(stateObj, mode),
|
||||
}))}
|
||||
.renderIcon=${this._renderOperationModeIcon}
|
||||
>
|
||||
@@ -103,7 +114,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
${supportAwayMode
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"away_mode"
|
||||
)}
|
||||
@@ -112,7 +123,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
@wa-select=${this._handleAwayModeChanged}
|
||||
.options=${["on", "off"].map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
label: this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"away_mode",
|
||||
mode
|
||||
@@ -165,7 +176,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
data.entity_id = this.stateObj!.entity_id;
|
||||
const curState = this.stateObj;
|
||||
|
||||
await this.hass.callService("water_heater", service, data);
|
||||
await this._api.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
|
||||
|
||||
@@ -13,6 +13,11 @@ 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;
|
||||
@@ -177,6 +182,12 @@ 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">
|
||||
@@ -189,16 +200,37 @@ class HaMoreInfoDetails extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
${attribute === "supported_features" && featureEnum
|
||||
? this._renderFeatures(featureEnum, this._stateObj!)
|
||||
: html`
|
||||
<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,11 +42,10 @@ export class MoreInfoLogbook extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.time=${this._time}
|
||||
.entityIds=${this._entityIdAsList(this.entityId)}
|
||||
.scope=${"entity"}
|
||||
narrow
|
||||
no-icon
|
||||
no-name
|
||||
show-indicator
|
||||
relative-time
|
||||
graph-color
|
||||
></ha-logbook>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ class DialogRestart extends LitElement {
|
||||
</ha-list-base>
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.restart.advanced_options"
|
||||
"ui.dialogs.restart.more_options"
|
||||
)}
|
||||
>
|
||||
<ha-list-base>
|
||||
|
||||
@@ -2,7 +2,6 @@ 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,15 +56,10 @@ class HaConfigAppsInfo extends LitElement {
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"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>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@@ -144,16 +138,11 @@ class HaConfigAppsInfo extends LitElement {
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 var(--ha-space-3);
|
||||
margin: 0;
|
||||
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,5 +1,7 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
@@ -30,6 +32,7 @@ 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";
|
||||
@@ -596,12 +599,30 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
const logbookColumn = html`
|
||||
${isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card outlined .header=${this.hass.localize("panel.logbook")}>
|
||||
<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-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._allEntities(memberships)}
|
||||
.deviceIds=${this._allDeviceIds(memberships.devices)}
|
||||
.scope=${"area"}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
@@ -979,6 +1000,22 @@ 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,6 +52,7 @@ 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: "", continue_on_timeout: true };
|
||||
return { wait_template: "" };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
@@ -44,6 +44,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
{
|
||||
name: "continue_on_timeout",
|
||||
selector: { boolean: {} },
|
||||
default: true,
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
@@ -3,16 +3,11 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import type {
|
||||
ForDict,
|
||||
PlatformCondition,
|
||||
} from "../../../../../data/automation";
|
||||
import type { PlatformCondition } from "../../../../../data/automation";
|
||||
import {
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
@@ -20,21 +15,11 @@ import {
|
||||
} from "../../../../../data/condition";
|
||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
||||
import { getRecordedEntity } from "../../../../../data/recorder";
|
||||
import type { TargetSelector } from "../../../../../data/selector";
|
||||
import {
|
||||
extractFromTarget,
|
||||
getTargetEntityCount,
|
||||
} from "../../../../../data/target";
|
||||
import { getTargetEntityCount } from "../../../../../data/target";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
|
||||
// Mirrors `MAX_HISTORY_PRIMING_LOOKBACK` in homeassistant/helpers/condition.py:
|
||||
// when a condition has a `for:` duration, the recorder is only queried this far
|
||||
// back to prime it at setup, so longer durations can't be fully satisfied from
|
||||
// history after a restart or reload.
|
||||
const MAX_HISTORY_PRIMING_LOOKBACK_HOURS = 6;
|
||||
|
||||
const showOptionalToggle = (field: ConditionDescription["fields"][string]) =>
|
||||
field.selector &&
|
||||
!field.required &&
|
||||
@@ -56,11 +41,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
|
||||
@state() private _resolvedTargetEntityCount?: number;
|
||||
|
||||
@state() private _targetHasUnrecordedEntity = false;
|
||||
|
||||
// Incremented on each recording check so stale async responses are ignored.
|
||||
private _recordingCheckToken = 0;
|
||||
|
||||
public static get defaultConfig(): PlatformCondition {
|
||||
return { condition: "" };
|
||||
}
|
||||
@@ -71,26 +51,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
this.hass.loadBackendTranslation("selector");
|
||||
}
|
||||
|
||||
// The `for:` priming info depends on both the condition (target + duration)
|
||||
// and the description (whether the condition targets entities at all), which
|
||||
// can arrive in separate updates.
|
||||
if (
|
||||
changedProperties.has("condition") ||
|
||||
changedProperties.has("description")
|
||||
) {
|
||||
const previousCondition = changedProperties.get("condition") as
|
||||
| undefined
|
||||
| this["condition"];
|
||||
if (
|
||||
changedProperties.has("description") ||
|
||||
previousCondition?.target !== this.condition?.target ||
|
||||
previousCondition?.options?.for !== this.condition?.options?.for
|
||||
) {
|
||||
this._updateDurationPrimingInfo();
|
||||
}
|
||||
}
|
||||
|
||||
if (!changedProperties.has("condition")) {
|
||||
return;
|
||||
}
|
||||
@@ -246,7 +206,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
conditionName
|
||||
)
|
||||
)}
|
||||
${this._renderDurationPrimingInfo()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -513,105 +472,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderDurationPrimingInfo() {
|
||||
const forValue = this.condition.options?.for;
|
||||
|
||||
// Priming only happens for entity conditions that have a `for:` duration.
|
||||
if (
|
||||
forValue === undefined ||
|
||||
forValue === "" ||
|
||||
!this.description?.target
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._targetHasUnrecordedEntity) {
|
||||
return html`<ha-alert alert-type="info" class="priming-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.duration_priming.entity_not_recorded"
|
||||
)}
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
if (this._durationExceedsLookback(forValue)) {
|
||||
return html`<ha-alert alert-type="info" class="priming-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.duration_priming.history_capped",
|
||||
{ hours: MAX_HISTORY_PRIMING_LOOKBACK_HOURS }
|
||||
)}
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _durationExceedsLookback(forValue: unknown): boolean {
|
||||
const duration = createDurationData(
|
||||
forValue as string | number | ForDict | undefined
|
||||
);
|
||||
if (!duration) {
|
||||
return false;
|
||||
}
|
||||
const seconds =
|
||||
(duration.days || 0) * 86400 +
|
||||
(duration.hours || 0) * 3600 +
|
||||
(duration.minutes || 0) * 60 +
|
||||
(duration.seconds || 0) +
|
||||
(duration.milliseconds || 0) / 1000;
|
||||
return seconds > MAX_HISTORY_PRIMING_LOOKBACK_HOURS * 3600;
|
||||
}
|
||||
|
||||
private async _updateDurationPrimingInfo(): Promise<void> {
|
||||
const forValue = this.condition.options?.for;
|
||||
const target = this.condition.target;
|
||||
|
||||
// Recording status only matters for an entity condition that has both a
|
||||
// target and a `for:` duration.
|
||||
const token = ++this._recordingCheckToken;
|
||||
if (
|
||||
forValue === undefined ||
|
||||
forValue === "" ||
|
||||
!this.description?.target ||
|
||||
!target ||
|
||||
!this.hass.config.components.includes("recorder")
|
||||
) {
|
||||
this._targetHasUnrecordedEntity = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { referenced_entities } = await extractFromTarget(
|
||||
this.hass.callWS,
|
||||
target
|
||||
);
|
||||
// Ignore if a newer check superseded this one.
|
||||
if (token !== this._recordingCheckToken) {
|
||||
return;
|
||||
}
|
||||
if (!referenced_entities.length) {
|
||||
this._targetHasUnrecordedEntity = false;
|
||||
return;
|
||||
}
|
||||
const recordingDisabled = await Promise.all(
|
||||
referenced_entities.map((entityId) =>
|
||||
getRecordedEntity(this.hass, entityId)
|
||||
.then((options) => options.recording_disabled_by !== null)
|
||||
// Unknown entity or command unavailable on older cores: don't warn.
|
||||
.catch(() => false)
|
||||
)
|
||||
);
|
||||
if (token !== this._recordingCheckToken) {
|
||||
return;
|
||||
}
|
||||
this._targetHasUnrecordedEntity = recordingDisabled.some(Boolean);
|
||||
} catch (_err) {
|
||||
// Target resolution failed; fall back to no warning rather than guessing.
|
||||
if (token === this._recordingCheckToken) {
|
||||
this._targetHasUnrecordedEntity = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -667,10 +527,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.priming-info {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -269,8 +269,9 @@ 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(Boolean);
|
||||
.map((lbl) => labelLookup!.get(lbl))
|
||||
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
|
||||
|
||||
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_description"
|
||||
"ui.panel.config.backup.data.share_folder_desc"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiCog,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
@@ -98,6 +100,7 @@ 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";
|
||||
@@ -900,12 +903,29 @@ export class HaConfigDevicePage extends LitElement {
|
||||
const logbookColumn = isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">${this.hass.localize("panel.logbook")}</h1>
|
||||
<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>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._entityIds(entities)}
|
||||
.deviceIds=${this._deviceIdInList(this.deviceId)}
|
||||
.scope=${"device"}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
@@ -1782,6 +1802,22 @@ 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,6 +231,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
}
|
||||
.form {
|
||||
padding: 20px 24px;
|
||||
z-index: 0;
|
||||
}
|
||||
.buttons {
|
||||
box-sizing: border-box;
|
||||
@@ -238,6 +239,9 @@ 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)!
|
||||
);
|
||||
const labelsEntries = (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
|
||||
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
|
||||
|
||||
const entityName = computeEntityEntryName(
|
||||
entry as EntityRegistryEntry,
|
||||
|
||||
@@ -44,7 +44,7 @@ class DialogScheduleBlockInfo extends DirtyStateProviderMixin<ScheduleBlockInfo>
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
{
|
||||
name: "advanced_settings",
|
||||
name: "more_options",
|
||||
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 "advanced_settings":
|
||||
case "more_options":
|
||||
return this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.generic.advanced_settings"
|
||||
"ui.dialogs.helper_settings.generic.more_options"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
|
||||
@@ -125,7 +125,7 @@ class HaCounterForm extends LitElement {
|
||||
></ha-input>
|
||||
<ha-expansion-panel
|
||||
header=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.generic.advanced_settings"
|
||||
"ui.dialogs.helper_settings.generic.more_options"
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
|
||||
@@ -93,7 +93,7 @@ class HaInputTextForm extends LitElement {
|
||||
></ha-icon-picker>
|
||||
<ha-expansion-panel
|
||||
header=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.generic.advanced_settings"
|
||||
"ui.dialogs.helper_settings.generic.more_options"
|
||||
)}
|
||||
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)!
|
||||
);
|
||||
const label_entries = (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
|
||||
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
|
||||
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)!
|
||||
);
|
||||
const label_entries = (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
|
||||
.filter((lbl): lbl is LabelRegistryEntry => lbl !== undefined);
|
||||
const assistants = getEntityVoiceAssistantsIds(
|
||||
entityReg,
|
||||
scene.entity_id
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user