Compare commits

..

1 Commits

Author SHA1 Message Date
Aidan Timson dccad52c07 Add typed query param helpers, add to history and activity 2026-06-05 11:55:17 +01:00
56 changed files with 1818 additions and 1825 deletions
+940
View File
File diff suppressed because one or more lines are too long
-944
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,4 +13,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.16.0.cjs
yarnPath: .yarn/releases/yarn-4.15.0.cjs
+48 -48
View File
@@ -94,55 +94,55 @@ class HaGallery extends LitElement {
<div slot="title">
${PAGES[this._page].metadata.title || this._page.split("/")[1]}
</div>
<div class="content">
${PAGES[this._page].description
? html`
<page-description .page=${this._page}></page-description>
`
: ""}
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
<div class="edit-docs">
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for
this page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
<div class="rtl-toggle">
<ha-icon-button
@click=${this._toggleRtl}
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
</ha-icon-button>
</div>
</div>
</ha-top-app-bar-fixed>
<div class="content">
${PAGES[this._page].description
? html`
<page-description .page=${this._page}></page-description>
`
: ""}
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
<div class="edit-docs">
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
<div class="rtl-toggle">
<ha-icon-button
@click=${this._toggleRtl}
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
</ha-icon-button>
</div>
</div>
</div>
</ha-drawer>
<notification-manager
@@ -1,48 +0,0 @@
---
title: "Brand Personality"
---
# Brand Personality
These five traits describe who Home Assistant is, not how it speaks. Tone of voice the playfulness, the informality, the warmth, etc should flow naturally from these, and will help guide writers on how to bring the brand personality to life.
The first four traits are relational: they describe how Home Assistant behaves toward its users.
_Welcoming_ is about how we receive them.\
_Candid_ is about how we communicate with them.\
_Supportive_ is about how we help them.\
_Generous_ is about how/what we give to them.\
If any of these feel similar, its because theyre all expressions of the same underlying character, just in different moments of the user relationship.
_Independent_ is different. Its foundational: it describes who Home Assistant is at its core.\
And its because of that independence that the other four traits feel genuine rather than performed. A corporate brand can try to be welcoming, candid, supportive, and generous,
but without independence, those traits will always be managed and moderated.
## Welcoming
**Warm and open, kind, friendly, approachable, accommodating**\
_But not: people pleasing, appeasing, sycophantic, ingratiating_\
Home Assistant feels like a knowledgeable friend, not a product. We meet you at your own level, never talk down to you, and make you feel valued regardless of your technical ability. This isnt performative, its expressed naturally in the small things: how errors are explained, documentation is written, and how the community talks to newcomers.
## Candid
**Direct, honest, transparent, unpretentious**\
_But not: unfriendly, rude, blunt, unempathetic_\
Home Assistant says what it means. We dont hide complexity behind false simplicity, fall back on marketing fluff, or pretend limitations dont exist. We respect users enough to be straight with them: about what Home Assistant can do, what it can't, and why it works the way it does. This is what builds real trust with users.
## Supportive
**Helpful, guiding, encouraging, empathetic, genuine**\
_But not: directive, hollow, condescending, over-bearing_\
Home Assistant always has your back. Whether youre just starting out or deep into a complex setup, we steer you forward without taking over. Our support is genuine: practical, patient, and there when its needed most. Because Home Assistant wants every user to succeed in building a smart home with privacy, choice, and sustainability at its heart.
## Generous
**Empowering, trusting, giving, sharing**\
_But not: overwhelming, indiscriminate, patronizing, controlling_\
Home Assistant gives you everything you need today, and as your setup evolves. There are no strings attached: no artificial limits, features locked behind tiers, or deceptive patterns designed to tie you to a closed platform. We trust users with control, access, and transparency. This generosity is reciprocal: the time, knowledge, and care our community gives freely is what keeps Home Assistant thriving and truly open.
## Independent
**Principled, liberated, confident, imperfect**\
_But not: conceited, obstinate, erratic, insular_\
Home Assistant doesnt feel the need to behave like an established tech brand or follow corporate rules. With no shareholders or VCs to answer to, we can say what we think, do things our own way, and not take ourselves too seriously. This freedom of spirit comes from the confidence of knowing our own values, and that our community shares them.
+3 -3
View File
@@ -93,7 +93,7 @@
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.5",
"idb-keyval": "6.2.4",
"intl-messageformat": "11.2.7",
"js-yaml": "4.2.0",
"leaflet": "1.9.4",
@@ -137,7 +137,7 @@
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.12",
"@rspack/core": "2.0.6",
"@rspack/core": "2.0.5",
"@rspack/dev-server": "2.0.3",
"@types/chromecast-caf-receiver": "6.0.26",
"@types/chromecast-caf-sender": "1.0.11",
@@ -212,7 +212,7 @@
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.16.0",
"packageManager": "yarn@4.15.0",
"volta": {
"node": "24.16.0"
}
+10 -1
View File
@@ -5,7 +5,7 @@ import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
(!page.filter || page.filter(hass));
isNotLoadedIntegration(hass, page);
export const isLoadedIntegration = (
hass: HomeAssistant,
@@ -16,4 +16,13 @@ export const isLoadedIntegration = (
isComponentLoaded(hass.config, integration)
);
export const isNotLoadedIntegration = (
hass: HomeAssistant,
page: PageNavigation
) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass.config, integration)
);
export const isCore = (page: PageNavigation) => page.core;
@@ -0,0 +1,73 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import {
createQueryString,
decodeQueryParams,
queryParamsFromServiceTarget,
serviceTargetFromQueryParams,
type QueryParamConfig,
type QueryParamValues,
type SearchParamsSource,
} from "./query-params";
export type HistoryLogbookTargetParamKey =
| "entity_id"
| "label_id"
| "floor_id"
| "area_id"
| "device_id";
export type HistoryLogbookDateParamKey = "start_date" | "end_date";
export type HistoryLogbookBooleanParamKey = "back";
export type HistoryLogbookQueryParams = QueryParamValues<
HistoryLogbookTargetParamKey,
HistoryLogbookDateParamKey,
HistoryLogbookBooleanParamKey
>;
export const historyLogbookTargetParamKeys: HistoryLogbookTargetParamKey[] = [
"entity_id",
"label_id",
"floor_id",
"area_id",
"device_id",
];
export const historyLogbookQueryParamConfig = {
list: historyLogbookTargetParamKeys,
date: ["start_date", "end_date"],
boolean: [{ key: "back", trueValue: "1" }],
} satisfies QueryParamConfig<
HistoryLogbookTargetParamKey,
HistoryLogbookDateParamKey,
HistoryLogbookBooleanParamKey
>;
export const decodeHistoryLogbookQueryParams = (
searchParams: SearchParamsSource
): HistoryLogbookQueryParams =>
decodeQueryParams(searchParams, historyLogbookQueryParamConfig);
export const historyLogbookTargetFromQueryParams = (
params: HistoryLogbookQueryParams
): HassServiceTarget | undefined =>
serviceTargetFromQueryParams(params, historyLogbookTargetParamKeys);
export const createHistoryLogbookUrl = (
path: string,
target: HassServiceTarget,
startDate: Date,
endDate: Date
): string => {
const queryString = createQueryString(
{
...queryParamsFromServiceTarget(target, historyLogbookTargetParamKeys),
start_date: startDate,
end_date: endDate,
},
historyLogbookQueryParamConfig
);
return queryString ? `${path}?${queryString}` : path;
};
+140
View File
@@ -0,0 +1,140 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { ensureArray } from "../array/ensure-array";
export type SearchParamsSource =
| URLSearchParams
| Record<string, string>
| string;
export interface QueryParamConfig<
ListKey extends string,
DateKey extends string,
BooleanKey extends string,
> {
list?: readonly ListKey[];
date?: readonly DateKey[];
boolean?: readonly {
key: BooleanKey;
trueValue: string;
}[];
}
export type QueryParamValues<
ListKey extends string,
DateKey extends string,
BooleanKey extends string,
> = Partial<
Record<ListKey, string[]> &
Record<DateKey, Date> &
Record<BooleanKey, boolean>
>;
export type ServiceTargetQueryParams<
Key extends keyof HassServiceTarget & string,
> = Partial<Record<Key, string[]>>;
const getSearchParam = (
searchParams: SearchParamsSource,
key: string
): string | null => {
if (typeof searchParams === "string") {
return new URLSearchParams(searchParams).get(key);
}
if (searchParams instanceof URLSearchParams) {
return searchParams.get(key);
}
return searchParams[key] ?? null;
};
export const decodeQueryParams = <
ListKey extends string,
DateKey extends string,
BooleanKey extends string,
>(
searchParams: SearchParamsSource,
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
): QueryParamValues<ListKey, DateKey, BooleanKey> => {
const params: QueryParamValues<ListKey, DateKey, BooleanKey> = {};
for (const key of config.list ?? []) {
const value = getSearchParam(searchParams, key);
if (value) {
params[key] = value.split(",") as (typeof params)[typeof key];
}
}
for (const key of config.date ?? []) {
const value = getSearchParam(searchParams, key);
if (value) {
params[key] = new Date(value) as (typeof params)[typeof key];
}
}
for (const { key, trueValue } of config.boolean ?? []) {
if (getSearchParam(searchParams, key) === trueValue) {
params[key] = true as (typeof params)[typeof key];
}
}
return params;
};
export const createQueryString = <
ListKey extends string,
DateKey extends string,
BooleanKey extends string,
>(
values: QueryParamValues<ListKey, DateKey, BooleanKey>,
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
): string => {
const searchParams = new URLSearchParams();
for (const key of config.list ?? []) {
const value = values[key] as string[] | undefined;
if (value?.length) {
searchParams.append(key, value.join(","));
}
}
for (const key of config.date ?? []) {
const value = values[key] as Date | undefined;
if (value) {
searchParams.append(key, value.toISOString());
}
}
for (const { key, trueValue } of config.boolean ?? []) {
if (values[key]) {
searchParams.append(key, trueValue);
}
}
return searchParams.toString();
};
export const serviceTargetFromQueryParams = <
Key extends keyof HassServiceTarget & string,
>(
params: ServiceTargetQueryParams<Key>,
keys: readonly Key[]
): HassServiceTarget | undefined => {
if (!keys.some((key) => params[key])) {
return undefined;
}
const target: HassServiceTarget = {};
for (const key of keys) {
const value = params[key];
if (value) {
target[key] = value;
}
}
return target;
};
export const queryParamsFromServiceTarget = <
Key extends keyof HassServiceTarget & string,
>(
target: HassServiceTarget,
keys: readonly Key[]
): ServiceTargetQueryParams<Key> => {
const params: ServiceTargetQueryParams<Key> = {};
for (const key of keys) {
const value = target[key];
if (value) {
params[key] = ensureArray(value);
}
}
return params;
};
+6 -9
View File
@@ -1,15 +1,12 @@
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } 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 type { HomeAssistant } from "../types";
import "./input/ha-input-multi";
@customElement("ha-aliases-editor")
class AliasesEditor extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public aliases!: string[];
@@ -28,9 +25,9 @@ class AliasesEditor extends LitElement {
.disabled=${this.disabled}
.sortable=${this.sortable}
update-on-blur
.label=${this._localize("ui.dialogs.aliases.label")}
.removeLabel=${this._localize("ui.dialogs.aliases.remove")}
.addLabel=${this._localize("ui.dialogs.aliases.add")}
.label=${this.hass!.localize("ui.dialogs.aliases.label")}
.removeLabel=${this.hass!.localize("ui.dialogs.aliases.remove")}
.addLabel=${this.hass!.localize("ui.dialogs.aliases.add")}
item-index
@value-changed=${this._aliasesChanged}
>
+5 -12
View File
@@ -1,16 +1,12 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { formattersContext } from "../data/context";
import type { HomeAssistant } from "../types";
@customElement("ha-attribute-value")
class HaAttributeValue extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@@ -57,7 +53,7 @@ class HaAttributeValue extends LitElement {
}
if (this.hideUnit) {
const parts = this._formatters!.formatEntityAttributeValueToParts(
const parts = this.hass.formatEntityAttributeValueToParts(
this.stateObj!,
this.attribute
);
@@ -67,10 +63,7 @@ class HaAttributeValue extends LitElement {
.join("");
}
return this._formatters!.formatEntityAttributeValue(
this.stateObj!,
this.attribute
);
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
}
static styles = css`
+1
View File
@@ -80,6 +80,7 @@ class HaAttributes extends LitElement {
</div>
<div class="value">
<ha-attribute-value
.hass=${this.hass}
.attribute=${attribute}
.stateObj=${this.stateObj}
></ha-attribute-value>
+20 -34
View File
@@ -1,22 +1,14 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { customElement, property } from "lit/decorators";
import type { ClimateEntity } from "../data/climate";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import { formattersContext } from "../data/context";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
class HaClimateState extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ClimateEntity;
@@ -32,7 +24,7 @@ class HaClimateState extends LitElement {
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this._formatters!.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"preset_mode"
)}`
@@ -45,7 +37,7 @@ class HaClimateState extends LitElement {
${currentStatus && !noValue
? html`
<div class="current">
${this._localize("ui.card.climate.currently")}:
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>
`
@@ -53,32 +45,32 @@ class HaClimateState extends LitElement {
}
private _computeCurrentStatus(): string | undefined {
if (!this._formatters || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (
this.stateObj.attributes.current_temperature != null &&
this.stateObj.attributes.current_humidity != null
) {
return `${this._formatters.formatEntityAttributeValue(
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}/
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
}
if (this.stateObj.attributes.current_temperature != null) {
return this._formatters.formatEntityAttributeValue(
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
);
}
if (this.stateObj.attributes.current_humidity != null) {
return this._formatters.formatEntityAttributeValue(
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
);
@@ -88,7 +80,7 @@ class HaClimateState extends LitElement {
}
private _computeTarget(): string {
if (!this._formatters || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return "";
}
@@ -96,39 +88,33 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null
) {
return `${this._formatters.formatEntityAttributeValue(
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_low"
)}-${this._formatters.formatEntityAttributeValue(
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_high"
)}`;
}
if (this.stateObj.attributes.temperature != null) {
return this._formatters.formatEntityAttributeValue(
this.stateObj,
"temperature"
);
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
}
if (
this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null
) {
return `${this._formatters.formatEntityAttributeValue(
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_low"
)}-${this._formatters.formatEntityAttributeValue(
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_high"
)}`;
}
if (this.stateObj.attributes.humidity != null) {
return this._formatters.formatEntityAttributeValue(
this.stateObj,
"humidity"
);
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
}
return "";
@@ -139,13 +125,13 @@ class HaClimateState extends LitElement {
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
return this._localize(`state.default.${this.stateObj.state}`);
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this._formatters!.formatEntityState(this.stateObj);
const stateString = this.hass.formatEntityState(this.stateObj);
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
const actionString = this._formatters!.formatEntityAttributeValue(
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"hvac_action"
);
+9 -15
View File
@@ -1,23 +1,17 @@
import { consume, type ContextType } from "@lit/context";
import { mdiStop } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
import { supportsFeature } from "../common/entity/supports-feature";
import type { LocalizeFunc } from "../common/translations/localize";
import { apiContext } from "../data/context";
import type { CoverEntity } from "../data/cover";
import { canClose, canOpen, canStop, CoverEntityFeature } from "../data/cover";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-cover-controls")
class HaCoverControls extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity;
@@ -32,7 +26,7 @@ class HaCoverControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.OPEN),
})}
.label=${this._localize("ui.card.cover.open_cover")}
.label=${this.hass.localize("ui.card.cover.open_cover")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${computeOpenIcon(this.stateObj)}
@@ -42,7 +36,7 @@ class HaCoverControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.STOP),
})}
.label=${this._localize("ui.card.cover.stop_cover")}
.label=${this.hass.localize("ui.card.cover.stop_cover")}
.path=${mdiStop}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
@@ -51,7 +45,7 @@ class HaCoverControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, CoverEntityFeature.CLOSE),
})}
.label=${this._localize("ui.card.cover.close_cover")}
.label=${this.hass.localize("ui.card.cover.close_cover")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${computeCloseIcon(this.stateObj)}
@@ -63,21 +57,21 @@ class HaCoverControls extends LitElement {
private _onOpenTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "open_cover", {
this.hass.callService("cover", "open_cover", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "close_cover", {
this.hass.callService("cover", "close_cover", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "stop_cover", {
this.hass.callService("cover", "stop_cover", {
entity_id: this.stateObj.entity_id,
});
}
+9 -15
View File
@@ -1,12 +1,8 @@
import { consume, type ContextType } from "@lit/context";
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } 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 { apiContext } from "../data/context";
import type { CoverEntity } from "../data/cover";
import {
canCloseTilt,
@@ -14,14 +10,12 @@ import {
canStopTilt,
CoverEntityFeature,
} from "../data/cover";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) stateObj!: CoverEntity;
@@ -37,7 +31,7 @@ class HaCoverTiltControls extends LitElement {
CoverEntityFeature.OPEN_TILT
),
})}
.label=${this._localize("ui.card.cover.open_tilt_cover")}
.label=${this.hass.localize("ui.card.cover.open_tilt_cover")}
.path=${mdiArrowTopRight}
@click=${this._onOpenTiltTap}
.disabled=${!canOpenTilt(this.stateObj)}
@@ -49,7 +43,7 @@ class HaCoverTiltControls extends LitElement {
CoverEntityFeature.STOP_TILT
),
})}
.label=${this._localize("ui.card.cover.stop_cover")}
.label=${this.hass.localize("ui.card.cover.stop_cover")}
.path=${mdiStop}
@click=${this._onStopTiltTap}
.disabled=${!canStopTilt(this.stateObj)}
@@ -61,7 +55,7 @@ class HaCoverTiltControls extends LitElement {
CoverEntityFeature.CLOSE_TILT
),
})}
.label=${this._localize("ui.card.cover.close_tilt_cover")}
.label=${this.hass.localize("ui.card.cover.close_tilt_cover")}
.path=${mdiArrowBottomLeft}
@click=${this._onCloseTiltTap}
.disabled=${!canCloseTilt(this.stateObj)}
@@ -70,21 +64,21 @@ class HaCoverTiltControls extends LitElement {
private _onOpenTiltTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "open_cover_tilt", {
this.hass.callService("cover", "open_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTiltTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "close_cover_tilt", {
this.hass.callService("cover", "close_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTiltTap(ev): void {
ev.stopPropagation();
this._api.callService("cover", "stop_cover_tilt", {
this.hass.callService("cover", "stop_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
}
+11 -10
View File
@@ -8,12 +8,13 @@ import { conditionalClamp } from "../common/number/clamp";
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public rows = 8;
@@ -34,10 +35,6 @@ export class HaGridSizeEditor extends LitElement {
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("value")) {
this._localValue = this.value;
@@ -65,7 +62,9 @@ export class HaGridSizeEditor extends LitElement {
return html`
<div class="grid">
<ha-grid-layout-slider
aria-label=${this._localize("ui.components.grid-size-picker.columns")}
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.columns"
)}
id="columns"
.min=${columnMin}
.max=${columnMax}
@@ -79,7 +78,9 @@ export class HaGridSizeEditor extends LitElement {
></ha-grid-layout-slider>
<ha-grid-layout-slider
aria-label=${this._localize("ui.components.grid-size-picker.rows")}
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.rows"
)}
id="rows"
.min=${rowMin}
.max=${rowMax}
@@ -97,10 +98,10 @@ export class HaGridSizeEditor extends LitElement {
@click=${this._reset}
class="reset"
.path=${mdiRestore}
label=${this._localize(
label=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
title=${this._localize(
title=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
>
+12 -20
View File
@@ -1,21 +1,13 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { formattersContext } from "../data/context";
import { customElement, property } from "lit/decorators";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import type { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@customElement("ha-humidifier-state")
class HaHumidifierState extends LitElement {
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HumidifierEntity;
@@ -30,7 +22,7 @@ class HaHumidifierState extends LitElement {
${this._localizeState()}
${this.stateObj.attributes.mode
? html`-
${this._formatters!.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"mode"
)}`
@@ -42,19 +34,19 @@ class HaHumidifierState extends LitElement {
${currentStatus && !noValue
? html`<div class="current">
${this._localize("ui.card.humidifier.currently")}:
${this.hass.localize("ui.card.humidifier.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
private _computeCurrentStatus(): string | undefined {
if (!this._formatters || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${this._formatters.formatEntityAttributeValue(
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
@@ -64,12 +56,12 @@ class HaHumidifierState extends LitElement {
}
private _computeTarget(): string {
if (!this._formatters || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return "";
}
if (this.stateObj.attributes.humidity != null) {
return `${this._formatters.formatEntityAttributeValue(
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"humidity"
)}`;
@@ -83,13 +75,13 @@ class HaHumidifierState extends LitElement {
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
return this._localize(`state.default.${this.stateObj.state}`);
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this._formatters!.formatEntityState(this.stateObj);
const stateString = this.hass.formatEntityState(this.stateObj);
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
const actionString = this._formatters!.formatEntityAttributeValue(
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"action"
);
+4 -7
View File
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@@ -16,15 +17,11 @@ export class HaIconButtonArrowNext extends LitElement {
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiArrowLeft : mdiArrowRight;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this._localize("ui.common.next") || "Next"}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
.path=${this._icon}
></ha-icon-button>
`;
+4 -7
View File
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@@ -24,15 +25,11 @@ export class HaIconButtonNext extends LitElement {
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this._localize("ui.common.next") || "Next"}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
+4 -7
View File
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-icon-button-prev")
export class HaIconButtonPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@@ -24,15 +25,11 @@ export class HaIconButtonPrev extends LitElement {
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this._localize("ui.common.back") || "Back"}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
+10 -11
View File
@@ -10,9 +10,8 @@ import type {
NetworkConfig,
} from "../data/network";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-checkbox";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
import "./ha-svg-icon";
@@ -42,14 +41,12 @@ declare global {
}
@customElement("ha-network")
export class HaNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public networkConfig?: NetworkConfig;
@state() private _expanded?: boolean;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render() {
if (this.networkConfig === undefined) {
return nothing;
@@ -60,14 +57,14 @@ export class HaNetwork extends LitElement {
@change=${this._handleAutoConfigureCheckboxClick}
.checked=${!configured_adapters.length}
.hint=${!configured_adapters.length
? this._localize(
? this.hass.localize(
"ui.panel.config.network.adapter.auto_configure_manual_hint"
)
: ""}
>
${this._localize("ui.panel.config.network.adapter.auto_configure")}
${this.hass.localize("ui.panel.config.network.adapter.auto_configure")}
<div class="description">
${this._localize("ui.panel.config.network.adapter.detected")}:
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</div>
</ha-checkbox>
@@ -80,11 +77,13 @@ export class HaNetwork extends LitElement {
.checked=${configured_adapters.includes(adapter.name)}
.adapter=${adapter.name}
>
${this._localize("ui.panel.config.network.adapter.adapter")}:
${this.hass.localize(
"ui.panel.config.network.adapter.adapter"
)}:
${adapter.name}
${adapter.default
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
(${this._localize("ui.common.default")})`
(${this.hass.localize("ui.common.default")})`
: nothing}
<div class="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
+4 -93
View File
@@ -1,6 +1,6 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
@@ -8,10 +8,6 @@ const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
export const haTopAppBarFixedStyles = css`
:host {
display: block;
--total-top-app-bar-height: calc(
var(--header-height, 0px) + var(--sub-row-height, 0px)
);
--sub-row-height: 0px;
}
.top-app-bar {
@@ -41,37 +37,14 @@ export const haTopAppBarFixedStyles = css`
}
.row {
box-sizing: border-box;
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
width: 100%;
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.top-app-bar.has-sub-row .row {
border-bottom: 0;
}
.sub-row {
box-sizing: border-box;
display: block;
width: 100%;
overflow: hidden;
border-bottom: var(--app-header-border-bottom);
}
.sub-row slot,
.sub-row ::slotted(*) {
box-sizing: border-box;
display: block;
width: 100%;
}
.sub-row[hidden] {
display: none;
}
.section {
display: flex;
align-items: center;
@@ -113,14 +86,8 @@ export const haTopAppBarFixedStyles = css`
}
.top-app-bar-fixed-adjust {
height: calc(
100vh - var(--total-top-app-bar-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
padding-top: calc(
var(--total-top-app-bar-height, 0px) + var(--safe-area-inset-top, 0px)
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
);
padding-bottom: var(--safe-area-inset-bottom);
padding-right: var(--safe-area-inset-right);
@@ -139,14 +106,8 @@ export class HaTopAppBarFixed extends LitElement {
@query(".top-app-bar") protected _barElement!: HTMLElement;
@query(".sub-row") protected _subRowElement?: HTMLElement;
@state() private _hasSubRow = false;
private _scrollTarget?: HTMLElement | Window;
private _subRowResizeObserver?: ResizeObserver;
@property({ attribute: false })
public get scrollTarget(): HTMLElement | Window {
return this._scrollTarget || window;
@@ -176,8 +137,6 @@ export class HaTopAppBarFixed extends LitElement {
super.connectedCallback();
if (this.hasUpdated) {
this._observeSubRowHeight();
this._updateSubRowHeight();
this._updateBarPosition();
this._registerListeners();
this._syncScrollState();
@@ -194,7 +153,6 @@ export class HaTopAppBarFixed extends LitElement {
<header
class="top-app-bar ${classMap({
"pane-header": paneHeader,
"has-sub-row": this._hasSubRow,
})}"
>
<div class="row">
@@ -218,9 +176,6 @@ export class HaTopAppBarFixed extends LitElement {
<slot name="actionItems"></slot>
</section>
</div>
<div class="sub-row" ?hidden=${!this._hasSubRow}>
<slot name="subRow" @slotchange=${this._subRowSlotChanged}></slot>
</div>
</header>
`;
}
@@ -233,23 +188,13 @@ export class HaTopAppBarFixed extends LitElement {
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
this._observeSubRowHeight();
this._updateSubRowHeight();
this._updateBarPosition();
this._registerListeners();
this._syncScrollState();
}
protected override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_hasSubRow")) {
this._updateSubRowHeight();
}
}
override disconnectedCallback() {
super.disconnectedCallback();
this._unobserveSubRowHeight();
this._unregisterListeners();
}
@@ -280,40 +225,6 @@ export class HaTopAppBarFixed extends LitElement {
this.scrollTarget.removeEventListener("scroll", this._syncScrollState);
}
private _observeSubRowHeight() {
if (
this._subRowResizeObserver ||
!this._subRowElement ||
typeof ResizeObserver === "undefined"
) {
return;
}
this._subRowResizeObserver = new ResizeObserver(this._updateSubRowHeight);
this._subRowResizeObserver.observe(this._subRowElement);
}
private _unobserveSubRowHeight() {
this._subRowResizeObserver?.disconnect();
this._subRowResizeObserver = undefined;
}
private _subRowSlotChanged = (ev: Event) => {
const slot = ev.currentTarget as HTMLSlotElement;
this._hasSubRow = slot
.assignedNodes({ flatten: true })
.some(
(node) =>
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim())
);
};
private _updateSubRowHeight = () => {
const subRowHeight = this._hasSubRow
? this._subRowElement?.offsetHeight || 0
: 0;
this.style.setProperty("--sub-row-height", `${subRowHeight}px`);
};
static override styles: CSSResultGroup = haTopAppBarFixedStyles;
}
@@ -108,9 +108,9 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
css`
.shadow-container {
position: absolute;
top: calc(-1 * var(--total-top-app-bar-height));
top: calc(-1 * var(--header-height));
width: 100%;
height: var(--total-top-app-bar-height);
height: var(--header-height);
z-index: 1;
transition: box-shadow 200ms linear;
}
@@ -131,7 +131,7 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
.top-app-bar-fixed-adjust--pane {
display: flex;
height: calc(
100vh - var(--total-top-app-bar-height, 0px) - var(
100vh - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
+9 -15
View File
@@ -1,22 +1,16 @@
import { consume, type ContextType } from "@lit/context";
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { LitElement, html, css, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } 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 { apiContext } from "../data/context";
import type { ValveEntity } from "../data/valve";
import { ValveEntityFeature, canClose, canOpen, canStop } from "../data/valve";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-valve-controls")
class HaValveControls extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
@@ -31,7 +25,7 @@ class HaValveControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
})}
.label=${this._localize("ui.card.valve.open_valve")}
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${mdiValveOpen}
@@ -41,7 +35,7 @@ class HaValveControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
})}
.label=${this._localize("ui.card.valve.stop_valve")}
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
.path=${mdiStop}
@@ -50,7 +44,7 @@ class HaValveControls extends LitElement {
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
})}
.label=${this._localize("ui.card.valve.close_valve")}
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${mdiValveClosed}
@@ -62,21 +56,21 @@ class HaValveControls extends LitElement {
private _onOpenTap(ev): void {
ev.stopPropagation();
this._api.callService("valve", "open_valve", {
this.hass.callService("valve", "open_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._api.callService("valve", "close_valve", {
this.hass.callService("valve", "close_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this._api.callService("valve", "stop_valve", {
this.hass.callService("valve", "stop_valve", {
entity_id: this.stateObj.entity_id,
});
}
-20
View File
@@ -1,20 +0,0 @@
import type { HomeAssistant } from "../types";
export interface RadioFrequencyTransmitter {
entity_id: string;
device_id: string | null;
config_entry_id: string | null;
supported_frequency_ranges: [number, number][];
supported_modulations: string[];
}
interface RadioFrequencyTransmitterList {
transmitters: RadioFrequencyTransmitter[];
}
export const fetchRadioFrequencyTransmitters = (
hass: HomeAssistant
): Promise<RadioFrequencyTransmitterList> =>
hass.callWS({
type: "radio_frequency/list",
});
@@ -123,7 +123,8 @@ class EntityPreviewRow extends LitElement {
const climateDomains = ["climate", "water_heater"];
if (climateDomains.includes(domain)) {
return html`
<ha-climate-state .stateObj=${stateObj}> </ha-climate-state>
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-climate-state>
`;
}
@@ -132,11 +133,15 @@ class EntityPreviewRow extends LitElement {
${isTiltOnly(stateObj)
? html`
<ha-cover-tilt-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-tilt-controls>
`
: html`
<ha-cover-controls .stateObj=${stateObj}></ha-cover-controls>
<ha-cover-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-controls>
`}
`;
}
@@ -211,7 +216,8 @@ class EntityPreviewRow extends LitElement {
if (domain === "humidifier") {
return html`
<ha-humidifier-state .stateObj=${stateObj}> </ha-humidifier-state>
<ha-humidifier-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-humidifier-state>
`;
}
@@ -190,6 +190,7 @@ class HaMoreInfoDetails extends LitElement {
</div>
<div class="value">
<ha-attribute-value
.hass=${this.hass}
.attribute=${attribute}
.stateObj=${this._stateObj}
></ha-attribute-value>
@@ -132,6 +132,7 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
</div>
<ha-icon-button-prev
slot="actionItems"
.hass=${this.hass}
@click=${this.closeDialog}
.label=${this.hass.localize("ui.notification_drawer.close")}
>
+1 -1
View File
@@ -33,10 +33,10 @@ export interface PageNavigation {
translationKey?: string;
component?: string | string[];
name?: string;
not_component?: string | string[];
core?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
filter?: (hass: HomeAssistant) => boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
@@ -206,6 +206,7 @@ class DialogAreaDetail
)}
</p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
@@ -229,6 +229,7 @@ class DialogFloorDetail extends LitElement {
)}
</p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
@@ -2,49 +2,20 @@ import { mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-arrow-prev";
import "../../../components/ha-menu-button";
import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-top-app-bar-fixed";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "./developer-tools-router";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const DEVELOPER_TOOLS_TABS = [
{
panel: "yaml",
translationKey: "ui.panel.config.developer-tools.tabs.yaml.title",
},
{
panel: "state",
translationKey: "ui.panel.config.developer-tools.tabs.states.title",
},
{
panel: "action",
translationKey: "ui.panel.config.developer-tools.tabs.actions.title",
},
{
panel: "template",
translationKey: "ui.panel.config.developer-tools.tabs.templates.title",
},
{
panel: "event",
translationKey: "ui.panel.config.developer-tools.tabs.events.title",
},
{
panel: "statistics",
translationKey: "ui.panel.config.developer-tools.tabs.statistics.title",
},
{
panel: "assist",
translationKey: "ui.panel.config.developer-tools.tabs.assist.tab",
},
] as const;
@customElement("ha-panel-developer-tools")
class PanelDeveloperTools extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -61,47 +32,94 @@ class PanelDeveloperTools extends LitElement {
protected render(): TemplateResult {
const page = this._page;
return html`
<ha-top-app-bar-fixed .narrow=${this.narrow}>
<ha-icon-button-arrow-prev
slot="navigationIcon"
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div slot="title">
${this.hass.localize(
"ui.panel.config.dashboard.developer_tools.main"
)}
</div>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="debug">
<div class="header ${classMap({ narrow: this.narrow })}">
<div class="toolbar">
<ha-icon-button-arrow-prev
slot="navigationIcon"
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div class="main-title">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
"ui.panel.config.dashboard.developer_tools.main"
)}
</ha-dropdown-item>
</ha-dropdown>
<ha-tab-group @wa-tab-show=${this._handlePageSelected} slot="subRow">
${DEVELOPER_TOOLS_TABS.map(
(tab) => html`
<ha-tab-group-tab
slot="nav"
panel=${tab.panel}
.active=${page === tab.panel}
>
${this.hass.localize(tab.translationKey)}
</ha-tab-group-tab>
`
)}
</div>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="debug">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
<ha-tab-group @wa-tab-show=${this._handlePageSelected}>
<ha-tab-group-tab slot="nav" panel="yaml" .active=${page === "yaml"}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="state"
.active=${page === "state"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.states.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="action"
.active=${page === "action"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="template"
.active=${page === "template"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="event"
.active=${page === "event"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="statistics"
.active=${page === "statistics"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.title"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
panel="assist"
.active=${page === "assist"}
>Assist</ha-tab-group-tab
>
</ha-tab-group>
<developer-tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
</ha-top-app-bar-fixed>
</div>
<developer-tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
`;
}
@@ -132,17 +150,90 @@ class PanelDeveloperTools extends LitElement {
navigate("/config");
}
static readonly styles: CSSResultGroup = css`
developer-tools-router {
display: block;
height: 100%;
}
ha-tab-group {
--ha-tab-active-text-color: var(--app-header-text-color, white);
--ha-tab-indicator-color: var(--app-header-text-color, white);
--ha-tab-track-color: transparent;
}
`;
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
color: var(--primary-text-color);
display: flex;
min-height: 100vh;
}
.header {
position: fixed;
top: 0;
z-index: 4;
background-color: var(--app-header-background-color);
width: calc(
var(--ha-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
color: var(--app-header-text-color, white);
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
}
:host([narrow]) .header {
width: calc(
var(--ha-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
.toolbar {
height: var(--header-height);
display: flex;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: var(--ha-space-2) var(--ha-space-3);
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: var(--ha-space-1);
}
.main-title {
margin-inline-start: var(--ha-space-6);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
.narrow .main-title {
margin-inline-start: var(--ha-space-2);
}
developer-tools-router {
display: block;
padding-top: calc(
var(--header-height) + 52px + var(--safe-area-inset-top, 0px)
);
padding-bottom: var(--safe-area-inset-bottom);
padding-right: var(--safe-area-inset-right);
flex: 1 1 100%;
max-width: calc(100% - var(--safe-area-inset-right, 0px));
}
:host([narrow]) developer-tools-router {
padding-left: var(--safe-area-inset-left);
max-width: calc(
100% - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
);
}
ha-tab-group {
--ha-tab-active-text-color: var(--app-header-text-color, white);
--ha-tab-indicator-color: var(--app-header-text-color, white);
--ha-tab-track-color: transparent;
border-bottom: var(--app-header-border-bottom, none);
}
`,
];
}
}
declare global {
@@ -602,6 +602,11 @@ class HaPanelDevState extends LitElement {
return [
haStyle,
css`
:host {
display: block;
height: 100%;
}
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-24
View File
@@ -29,14 +29,12 @@ import {
mdiTextBoxOutline,
mdiTools,
mdiUpdate,
mdiVideoInputAntenna,
mdiViewDashboard,
mdiZigbee,
mdiZWave,
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import type { CloudStatus } from "../../data/cloud";
@@ -57,14 +55,6 @@ declare global {
}
}
const getHasDomainCheck = (domain: string) => {
const prefix = `${domain}.`;
const checkRegistry = memoizeOne((entries: HomeAssistant["entities"]) =>
Object.values(entries).some((entry) => entry.entity_id.startsWith(prefix))
);
return (hass: HomeAssistant) => checkRegistry(hass.entities);
};
export const configSections: Record<string, PageNavigation[]> = {
dashboard: [
{
@@ -176,15 +166,6 @@ export const configSections: Record<string, PageNavigation[]> = {
translationKey: "bluetooth",
adminOnly: true,
},
{
path: "/config/radio-frequency",
iconPath: mdiVideoInputAntenna,
iconColor: "#E74011",
component: "radio_frequency",
translationKey: "radio_frequency",
adminOnly: true,
filter: getHasDomainCheck("radio_frequency"),
},
{
path: "/insteon",
iconPath:
@@ -686,11 +667,6 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),
},
"radio-frequency": {
tag: "radio-frequency-config-dashboard",
load: () =>
import("./integrations/integration-panels/radio_frequency/radio-frequency-config-dashboard"),
},
repairs: {
tag: "ha-config-repairs-dashboard",
load: () => import("./repairs/ha-config-repairs-dashboard"),
@@ -1003,10 +1003,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
if (!entityReg) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.helpers.picker.no_category_support"
"ui.panel.config.automation.picker.no_category_support"
),
text: this.hass.localize(
"ui.panel.config.helpers.picker.no_category_entity_reg"
"ui.panel.config.automation.picker.no_category_entity_reg"
),
});
return;
@@ -1,171 +0,0 @@
import { mdiRadioTower } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-relative-time";
import type { RadioFrequencyTransmitter } from "../../../../../data/radio_frequency";
import { fetchRadioFrequencyTransmitters } from "../../../../../data/radio_frequency";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import { computeEntityName } from "../../../../../common/entity/compute_entity_name";
@customElement("radio-frequency-config-dashboard")
export class RadioFrequencyConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _transmitters: RadioFrequencyTransmitter[] = [];
public firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._fetchTransmitters();
}
private async _fetchTransmitters(): Promise<void> {
const result = await fetchRadioFrequencyTransmitters(this.hass);
this._transmitters = result.transmitters;
}
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.radio_frequency.title")}
back-path="/config"
>
<div class="container">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.radio_frequency.transmitters_count",
{ count: this._transmitters.length }
)}
>
<div class="card-content">
${this._transmitters.length === 0
? html`<p class="no-transmitters">
${this.hass.localize(
"ui.panel.config.radio_frequency.no_transmitters"
)}
</p>`
: html`
<ha-md-list>
${this._transmitters.map((transmitter) =>
this._renderTransmitter(transmitter)
)}
</ha-md-list>
`}
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _renderTransmitter(
transmitter: RadioFrequencyTransmitter
): TemplateResult {
const entityState = this.hass.states[transmitter.entity_id];
const entity = this.hass.entities[transmitter.entity_id];
const device = transmitter.device_id
? this.hass.devices[transmitter.device_id]
: undefined;
const areaId = entity.area_id || (device ? device.area_id : undefined);
const area = areaId ? this.hass.areas[areaId] : undefined;
return html`
<ha-md-list-item
type=${device ? "link" : "text"}
href=${device
? `/config/devices/device/${transmitter.device_id}`
: nothing}
>
<ha-svg-icon slot="start" .path=${mdiRadioTower}></ha-svg-icon>
<div slot="headline">
${device
? computeDeviceName(device)
: computeEntityName(
this.hass.states[transmitter.entity_id],
this.hass.entities,
this.hass.devices
)}
</div>
<div slot="supporting-text">
${area ? `${area.name} · ` : ""}
${transmitter.supported_frequency_ranges
.map(
([min, max]) =>
`${parseFloat((min / 1000000).toFixed(2))}-${parseFloat((max / 1000000).toFixed(2))}MHz`
)
.join(", ")}
· ${transmitter.supported_modulations.join(", ")}
</div>
${device
? html`<div slot="end">
${this.hass.localize(
"ui.panel.config.radio_frequency.last_used"
)}:
<br />
${entityState.state === "unknown" ||
entityState.state === "unavailable"
? this.hass.localize(`state.default.${entityState.state}`)
: html`
<ha-relative-time
.datetime=${entityState.state}
></ha-relative-time>
`}
</div>`
: nothing}
</ha-md-list-item>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
ha-card {
margin: 0px auto var(--ha-space-4);
max-width: 600px;
}
ha-card .card-content {
padding: 0;
}
ha-md-list {
background: none;
padding: 0;
}
.no-transmitters {
padding: var(--ha-space-4);
margin: 0;
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"radio-frequency-config-dashboard": RadioFrequencyConfigDashboard;
}
}
@@ -46,6 +46,7 @@ class ConfigNetwork extends LitElement {
</p>
<ha-network
@network-config-changed=${this._configChanged}
.hass=${this.hass}
.networkConfig=${this._networkConfig}
></ha-network>
</div>
@@ -304,6 +304,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
></ha-switch>
</ha-md-list-item>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${(this._aliases ?? this.entry.aliases).filter(
(a): a is string => a !== null
)}
+24 -68
View File
@@ -13,13 +13,16 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { computeDomain } from "../../common/entity/compute_domain";
import { goBack, navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
createSearchParam,
createHistoryLogbookUrl,
decodeHistoryLogbookQueryParams,
historyLogbookTargetFromQueryParams,
} from "../../common/url/history-logbook-query-params";
import {
extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params";
@@ -230,43 +233,18 @@ class HaPanelHistory extends LitElement {
return;
}
const searchParams = extractSearchParamsObject();
const entityIds = searchParams.entity_id;
const deviceIds = searchParams.device_id;
const areaIds = searchParams.area_id;
const floorIds = searchParams.floor_id;
const labelsIds = searchParams.label_id;
if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
this._targetPickerValue = {};
const queryParams = decodeHistoryLogbookQueryParams(
extractSearchParamsObject()
);
const targetPickerValue = historyLogbookTargetFromQueryParams(queryParams);
if (targetPickerValue) {
this._targetPickerValue = targetPickerValue;
}
if (entityIds) {
const splitIds = entityIds.split(",");
this._targetPickerValue!.entity_id = splitIds;
if (queryParams.start_date) {
this._startDate = queryParams.start_date;
}
if (deviceIds) {
const splitIds = deviceIds.split(",");
this._targetPickerValue!.device_id = splitIds;
}
if (areaIds) {
const splitIds = areaIds.split(",");
this._targetPickerValue!.area_id = splitIds;
}
if (floorIds) {
const splitIds = floorIds.split(",");
this._targetPickerValue!.floor_id = splitIds;
}
if (labelsIds) {
const splitIds = labelsIds.split(",");
this._targetPickerValue!.label_id = splitIds;
}
const startDate = searchParams.start_date;
if (startDate) {
this._startDate = new Date(startDate);
}
const endDate = searchParams.end_date;
if (endDate) {
this._endDate = new Date(endDate);
if (queryParams.end_date) {
this._endDate = queryParams.end_date;
}
}
@@ -469,37 +447,15 @@ class HaPanelHistory extends LitElement {
}
private _updatePath() {
const params: Record<string, string> = {};
if (this._targetPickerValue.entity_id) {
params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
","
);
}
if (this._targetPickerValue.label_id) {
params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
}
if (this._targetPickerValue.floor_id) {
params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
}
if (this._targetPickerValue.area_id) {
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
}
if (this._targetPickerValue.device_id) {
params.device_id = ensureArray(this._targetPickerValue.device_id).join(
","
);
}
if (this._startDate) {
params.start_date = this._startDate.toISOString();
}
if (this._endDate) {
params.end_date = this._endDate.toISOString();
}
navigate(`/history?${createSearchParam(params)}`, { replace: true });
navigate(
createHistoryLogbookUrl(
"/history",
this._targetPickerValue,
this._startDate,
this._endDate
),
{ replace: true }
);
}
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
+25 -71
View File
@@ -4,12 +4,15 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { goBack, navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
createSearchParam,
createHistoryLogbookUrl,
decodeHistoryLogbookQueryParams,
historyLogbookTargetFromQueryParams,
} from "../../common/url/history-logbook-query-params";
import {
extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params";
@@ -185,44 +188,17 @@ export class HaPanelLogbook extends LitElement {
);
private _applyURLParams() {
const searchParams = extractSearchParamsObject();
const entityIds = searchParams.entity_id;
const deviceIds = searchParams.device_id;
const areaIds = searchParams.area_id;
const floorIds = searchParams.floor_id;
const labelsIds = searchParams.label_id;
if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
this._targetPickerValue = {};
}
if (entityIds) {
const splitIds = entityIds.split(",");
this._targetPickerValue!.entity_id = splitIds;
}
if (deviceIds) {
const splitIds = deviceIds.split(",");
this._targetPickerValue!.device_id = splitIds;
}
if (areaIds) {
const splitIds = areaIds.split(",");
this._targetPickerValue!.area_id = splitIds;
}
if (floorIds) {
const splitIds = floorIds.split(",");
this._targetPickerValue!.floor_id = splitIds;
}
if (labelsIds) {
const splitIds = labelsIds.split(",");
this._targetPickerValue!.label_id = splitIds;
const queryParams = decodeHistoryLogbookQueryParams(
extractSearchParamsObject()
);
const targetPickerValue = historyLogbookTargetFromQueryParams(queryParams);
if (targetPickerValue) {
this._targetPickerValue = targetPickerValue;
}
const startDateStr = searchParams.start_date;
const endDateStr = searchParams.end_date;
if (startDateStr || endDateStr) {
const startDate = startDateStr
? new Date(startDateStr)
: this._time.range[0];
const endDate = endDateStr ? new Date(endDateStr) : this._time.range[1];
if (queryParams.start_date || queryParams.end_date) {
const startDate = queryParams.start_date ?? this._time.range[0];
const endDate = queryParams.end_date ?? this._time.range[1];
// Only set if date has changed.
if (
@@ -231,8 +207,8 @@ export class HaPanelLogbook extends LitElement {
) {
this._time = {
range: [
startDateStr ? new Date(startDateStr) : this._time.range[0],
endDateStr ? new Date(endDateStr) : this._time.range[1],
queryParams.start_date ?? this._time.range[0],
queryParams.end_date ?? this._time.range[1],
],
};
}
@@ -254,37 +230,15 @@ export class HaPanelLogbook extends LitElement {
}
private _updatePath() {
const params: Record<string, string> = {};
if (this._targetPickerValue.entity_id) {
params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
","
);
}
if (this._targetPickerValue.label_id) {
params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
}
if (this._targetPickerValue.floor_id) {
params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
}
if (this._targetPickerValue.area_id) {
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
}
if (this._targetPickerValue.device_id) {
params.device_id = ensureArray(this._targetPickerValue.device_id).join(
","
);
}
if (this._time.range[0]) {
params.start_date = this._time.range[0].toISOString();
}
if (this._time.range[1]) {
params.end_date = this._time.range[1].toISOString();
}
navigate(`/logbook?${createSearchParam(params)}`, { replace: true });
navigate(
createHistoryLogbookUrl(
"/logbook",
this._targetPickerValue,
this._time.range[0],
this._time.range[1]
),
{ replace: true }
);
}
private _refreshLogbook() {
@@ -181,6 +181,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? stateObj.attributes[this._config.attribute!] !== undefined
? html`<ha-attribute-value
hide-unit
.hass=${this.hass}
.stateObj=${stateObj}
.attribute=${this._config.attribute!}
>
@@ -133,6 +133,7 @@ export class HuiCardLayoutEditor extends LitElement {
"max-width": `${(this.sectionConfig.column_span ?? 1) * 250 + 40}px`,
})}
.columns=${gridTotalColumns}
.hass=${this.hass}
.value=${gridValue}
.isDefault=${this._isDefault(configOptions)}
@value-changed=${this._gridSizeChanged}
@@ -43,7 +43,8 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<ha-climate-state .stateObj=${stateObj}> </ha-climate-state>
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-climate-state>
</hui-generic-entity-row>
`;
}
@@ -48,11 +48,15 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow {
${isTiltOnly(stateObj)
? html`
<ha-cover-tilt-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-tilt-controls>
`
: html`
<ha-cover-controls .stateObj=${stateObj}></ha-cover-controls>
<ha-cover-controls
.hass=${this.hass}
.stateObj=${stateObj}
></ha-cover-controls>
`}
</hui-generic-entity-row>
`;
@@ -45,7 +45,8 @@ class HuiHumidifierEntityRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<ha-humidifier-state .stateObj=${stateObj}> </ha-humidifier-state>
<ha-humidifier-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-humidifier-state>
</hui-generic-entity-row>
`;
}
-3
View File
@@ -136,10 +136,7 @@ class LovelaceFullConfigEditor extends LitElement {
}
ha-yaml-editor {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.save-button {
+1
View File
@@ -522,6 +522,7 @@ class HUIRoot extends LitElement {
@click=${this._editView}
></ha-icon-button>
<ha-icon-button-arrow-next
.hass=${this.hass}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.move_right"
)}
@@ -69,6 +69,7 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
<ha-attribute-value
.hideUnit=${this._config.suffix !== undefined &&
this._config.suffix !== ""}
.hass=${this.hass}
.stateObj=${stateObj}
.attribute=${this._config.attribute}
>
-3
View File
@@ -296,9 +296,6 @@ export const getMyRedirects = (): Redirects => ({
component: "history",
redirect: "/history",
},
maintenance: {
redirect: "/maintenance",
},
overview: {
redirect: "/home/overview",
},
+4 -1
View File
@@ -23,7 +23,10 @@ class StateCardClimate extends LitElement {
.stateObj=${this.stateObj}
.inDialog=${this.inDialog}
></state-info>
<ha-climate-state .stateObj=${this.stateObj}></ha-climate-state>
<ha-climate-state
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-climate-state>
</div>
`;
}
+2
View File
@@ -26,10 +26,12 @@ class StateCardCover extends LitElement {
.inDialog=${this.inDialog}
></state-info>
<ha-cover-controls
.hass=${this.hass}
.hidden=${isTiltOnly(this.stateObj)}
.stateObj=${this.stateObj}
></ha-cover-controls>
<ha-cover-tilt-controls
.hass=${this.hass}
.hidden=${!isTiltOnly(this.stateObj)}
.stateObj=${this.stateObj}
></ha-cover-tilt-controls>
+4 -1
View File
@@ -25,7 +25,10 @@ class StateCardHumidifier extends LitElement {
.inDialog=${this.inDialog}
>
</state-info>
<ha-humidifier-state .stateObj=${this.stateObj}></ha-humidifier-state>
<ha-humidifier-state
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-humidifier-state>
</div>
`;
}
+1 -15
View File
@@ -1616,7 +1616,6 @@
"zwave_js": "[%key:ui::panel::config::dashboard::zwave_js::main%]",
"thread": "[%key:ui::panel::config::dashboard::thread::main%]",
"bluetooth": "[%key:ui::panel::config::dashboard::bluetooth::main%]",
"radio_frequency": "[%key:ui::panel::config::dashboard::radio_frequency::main%]",
"knx": "[%key:ui::panel::config::dashboard::knx::main%]",
"insteon": "[%key:ui::panel::config::dashboard::insteon::main%]",
"voice-assistants": "[%key:ui::panel::config::dashboard::voice_assistants::main%]",
@@ -2719,10 +2718,6 @@
"main": "Bluetooth",
"secondary": "Local device connectivity"
},
"radio_frequency": {
"main": "Radio frequency",
"secondary": "Control radio-based devices."
},
"knx": {
"main": "KNX",
"secondary": "Building automation standard"
@@ -3837,7 +3832,6 @@
"developer-tools": {
"tabs": {
"assist": {
"tab": "Assist",
"title": "Sentences parser",
"description": "Enter sentences and see how they will be parsed by Home Assistant. Each line will be processed as an individual sentence. Intents will not be executed on your instance.",
"parse_sentences": "Parse sentences",
@@ -4394,9 +4388,7 @@
"error_information": "Error information",
"delete_confirm_title": "Delete helper?",
"delete_confirm_text": "Are you sure you want to delete {name}?",
"delete_failed": "Failed to delete helper",
"no_category_support": "You can't assign a category to this helper",
"no_category_entity_reg": "To assign a category to a helper it needs to have a unique ID."
"delete_failed": "Failed to delete helper"
},
"dialog": {
"create": "Create",
@@ -7192,12 +7184,6 @@
"known_devices": "Known devices",
"unknown_devices": "Unknown devices"
},
"radio_frequency": {
"title": "Radio frequency",
"transmitters_count": "{count} {count, plural,\n one {transmitter}\n other {transmitters}\n}",
"no_transmitters": "No radio frequency transmitters found",
"last_used": "Last used"
},
"dhcp": {
"title": "DHCP discovery",
"mac_address": "MAC address",
+23
View File
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
canShowPage,
isLoadedIntegration,
isNotLoadedIntegration,
isCore,
} from "../../../src/common/config/can_show_page";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
@@ -49,6 +50,28 @@ describe("isLoadedIntegration", () => {
});
});
describe("isNotLoadedIntegration", () => {
it("should return true if the integration is not loaded", () => {
const hass = {
config: { components: ["test_component"] },
} as unknown as HomeAssistant;
const page = {
not_component: "other_component",
} as unknown as PageNavigation;
expect(isNotLoadedIntegration(hass, page)).toBe(true);
});
it("should return false if the integration is loaded", () => {
const hass = {
config: { components: ["test_component"] },
} as unknown as HomeAssistant;
const page = {
not_component: "test_component",
} as unknown as PageNavigation;
expect(isNotLoadedIntegration(hass, page)).toBe(false);
});
});
describe("isCore", () => {
it("should return true if the page is core", () => {
const page = { core: true } as unknown as PageNavigation;
+146
View File
@@ -0,0 +1,146 @@
import { describe, expect, it } from "vitest";
import {
createHistoryLogbookUrl,
decodeHistoryLogbookQueryParams,
historyLogbookQueryParamConfig,
historyLogbookTargetParamKeys,
historyLogbookTargetFromQueryParams,
} from "../../../src/common/url/history-logbook-query-params";
import {
createQueryString,
decodeQueryParams,
queryParamsFromServiceTarget,
serviceTargetFromQueryParams,
} from "../../../src/common/url/query-params";
const panelQueryParams = [
{
type: "history",
path: "/history",
},
{
type: "logbook",
path: "/logbook",
},
] as const;
describe.each(panelQueryParams)("$type query params", (panel) => {
it("decodes target and date params", () => {
const params = decodeQueryParams(
"?entity_id=light.kitchen,switch.fan&device_id=device-1&area_id=kitchen&floor_id=downstairs&label_id=important&start_date=2026-06-05T10:00:00.000Z&end_date=2026-06-05T11:00:00.000Z&back=1",
historyLogbookQueryParamConfig
);
expect(params).toEqual({
entity_id: ["light.kitchen", "switch.fan"],
label_id: ["important"],
floor_id: ["downstairs"],
area_id: ["kitchen"],
device_id: ["device-1"],
start_date: new Date("2026-06-05T10:00:00.000Z"),
end_date: new Date("2026-06-05T11:00:00.000Z"),
back: true,
});
});
it("creates target picker values only when target params are present", () => {
expect(
serviceTargetFromQueryParams(
decodeQueryParams(
"?start_date=2026-06-05T10:00:00.000Z",
historyLogbookQueryParamConfig
),
historyLogbookTargetParamKeys
)
).toBeUndefined();
expect(
serviceTargetFromQueryParams(
decodeQueryParams(
"?entity_id=light.kitchen&area_id=kitchen",
historyLogbookQueryParamConfig
),
historyLogbookTargetParamKeys
)
).toEqual({
entity_id: ["light.kitchen"],
area_id: ["kitchen"],
});
});
it("ignores empty target values", () => {
expect(
serviceTargetFromQueryParams(
decodeQueryParams(
"?entity_id=&device_id=",
historyLogbookQueryParamConfig
),
historyLogbookTargetParamKeys
)
).toBeUndefined();
});
it("encodes target picker values", () => {
expect(
queryParamsFromServiceTarget(
{
entity_id: ["light.kitchen", "switch.fan"],
area_id: "kitchen",
},
historyLogbookTargetParamKeys
)
).toEqual({
entity_id: ["light.kitchen", "switch.fan"],
area_id: ["kitchen"],
});
});
it("creates deterministic query strings", () => {
expect(
createQueryString(
{
device_id: ["device-1"],
entity_id: ["light.kitchen"],
start_date: new Date("2026-06-05T10:00:00.000Z"),
end_date: new Date("2026-06-05T11:00:00.000Z"),
},
historyLogbookQueryParamConfig
)
).toBe(
"entity_id=light.kitchen&device_id=device-1&start_date=2026-06-05T10%3A00%3A00.000Z&end_date=2026-06-05T11%3A00%3A00.000Z"
);
});
it("creates typed URLs", () => {
expect(
createHistoryLogbookUrl(
panel.path,
{ entity_id: ["light.kitchen"] },
new Date("2026-06-05T10:00:00.000Z"),
new Date("2026-06-05T11:00:00.000Z")
)
).toBe(
`${panel.path}?entity_id=light.kitchen&start_date=2026-06-05T10%3A00%3A00.000Z&end_date=2026-06-05T11%3A00%3A00.000Z`
);
});
});
describe("history logbook query params", () => {
it("decodes query params", () => {
expect(
decodeHistoryLogbookQueryParams("?entity_id=light.kitchen&back=1")
).toEqual({
entity_id: ["light.kitchen"],
back: true,
});
});
it("creates target picker values only when target params are present", () => {
expect(
historyLogbookTargetFromQueryParams(
decodeHistoryLogbookQueryParams("?start_date=2026-06-05T10:00:00.000Z")
)
).toBeUndefined();
});
});
+55 -55
View File
@@ -3591,51 +3591,51 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-darwin-arm64@npm:2.0.6"
"@rspack/binding-darwin-arm64@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-darwin-arm64@npm:2.0.5"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-darwin-x64@npm:2.0.6"
"@rspack/binding-darwin-x64@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-darwin-x64@npm:2.0.5"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.0.6"
"@rspack/binding-linux-arm64-gnu@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.0.5"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-linux-arm64-musl@npm:2.0.6"
"@rspack/binding-linux-arm64-musl@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-linux-arm64-musl@npm:2.0.5"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-linux-x64-gnu@npm:2.0.6"
"@rspack/binding-linux-x64-gnu@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-linux-x64-gnu@npm:2.0.5"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-linux-x64-musl@npm:2.0.6"
"@rspack/binding-linux-x64-musl@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-linux-x64-musl@npm:2.0.5"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-wasm32-wasi@npm:2.0.6"
"@rspack/binding-wasm32-wasi@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-wasm32-wasi@npm:2.0.5"
dependencies:
"@emnapi/core": "npm:1.10.0"
"@emnapi/runtime": "npm:1.10.0"
@@ -3644,41 +3644,41 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.0.6"
"@rspack/binding-win32-arm64-msvc@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.0.5"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.0.6"
"@rspack/binding-win32-ia32-msvc@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.0.5"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding-win32-x64-msvc@npm:2.0.6"
"@rspack/binding-win32-x64-msvc@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding-win32-x64-msvc@npm:2.0.5"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/binding@npm:2.0.6"
"@rspack/binding@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/binding@npm:2.0.5"
dependencies:
"@rspack/binding-darwin-arm64": "npm:2.0.6"
"@rspack/binding-darwin-x64": "npm:2.0.6"
"@rspack/binding-linux-arm64-gnu": "npm:2.0.6"
"@rspack/binding-linux-arm64-musl": "npm:2.0.6"
"@rspack/binding-linux-x64-gnu": "npm:2.0.6"
"@rspack/binding-linux-x64-musl": "npm:2.0.6"
"@rspack/binding-wasm32-wasi": "npm:2.0.6"
"@rspack/binding-win32-arm64-msvc": "npm:2.0.6"
"@rspack/binding-win32-ia32-msvc": "npm:2.0.6"
"@rspack/binding-win32-x64-msvc": "npm:2.0.6"
"@rspack/binding-darwin-arm64": "npm:2.0.5"
"@rspack/binding-darwin-x64": "npm:2.0.5"
"@rspack/binding-linux-arm64-gnu": "npm:2.0.5"
"@rspack/binding-linux-arm64-musl": "npm:2.0.5"
"@rspack/binding-linux-x64-gnu": "npm:2.0.5"
"@rspack/binding-linux-x64-musl": "npm:2.0.5"
"@rspack/binding-wasm32-wasi": "npm:2.0.5"
"@rspack/binding-win32-arm64-msvc": "npm:2.0.5"
"@rspack/binding-win32-ia32-msvc": "npm:2.0.5"
"@rspack/binding-win32-x64-msvc": "npm:2.0.5"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -3700,15 +3700,15 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/c2e5245abab3257d02f5d98947fad26c8de1b18bb17362734035cfbdd725d9c6c78432372bdff985b32fa4062059d7210e9f5ea7314ae3080805b64f616fe348
checksum: 10/95b4fa9daf1935e2ca3d1bd08b12c7f26ef0fffb09d7764bcb11fed1d4ecedc4dd79cbfbb37519121e81c06b97ade27f2d7a33ed0ed51e3b7d650b37b8b33c9e
languageName: node
linkType: hard
"@rspack/core@npm:2.0.6":
version: 2.0.6
resolution: "@rspack/core@npm:2.0.6"
"@rspack/core@npm:2.0.5":
version: 2.0.5
resolution: "@rspack/core@npm:2.0.5"
dependencies:
"@rspack/binding": "npm:2.0.6"
"@rspack/binding": "npm:2.0.5"
peerDependencies:
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
"@swc/helpers": ^0.5.23
@@ -3717,7 +3717,7 @@ __metadata:
optional: true
"@swc/helpers":
optional: true
checksum: 10/d2417690e8135342179bc9e5035e16fe827522b4c0babef029a21ff5903cd56c09b86f08924527bd7d3e66f178f1f678ce099199cac8c1a137b18c5d8892e613
checksum: 10/b955a12bd04e1e3bef954f8a3cfd48c7302d114fee4d13dc7c6f46645bb18d377a4df7f86520bbf44519ed7bb315bd119e98768bfd7d851b1329ba9ee51ce1ac
languageName: node
linkType: hard
@@ -8483,7 +8483,7 @@ __metadata:
"@octokit/rest": "npm:22.0.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.5.12"
"@rspack/core": "npm:2.0.6"
"@rspack/core": "npm:2.0.5"
"@rspack/dev-server": "npm:2.0.3"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -8548,7 +8548,7 @@ __metadata:
home-assistant-js-websocket: "npm:9.6.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.5"
idb-keyval: "npm:6.2.4"
intl-messageformat: "npm:11.2.7"
js-yaml: "npm:4.2.0"
jsdom: "npm:29.1.1"
@@ -8762,10 +8762,10 @@ __metadata:
languageName: node
linkType: hard
"idb-keyval@npm:6.2.5":
version: 6.2.5
resolution: "idb-keyval@npm:6.2.5"
checksum: 10/ac645882b3258ff07347d085baab91b871bac7be4f46ff8e20a7c036c2df35d3f695a30050009f27237b99045203568f2a842a35295a48f9b815959ee51a347e
"idb-keyval@npm:6.2.4":
version: 6.2.4
resolution: "idb-keyval@npm:6.2.4"
checksum: 10/b1bc874eb582c6bed89dd40a07fe5ca593238b37cded9c604e0cb74b396d2b8caa850519af4467e5ca1b4628682a6102150299db69a393702d0a0718945bc5ec
languageName: node
linkType: hard