Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein 439e103aef List main entities first on the device page 2026-06-18 14:23:40 +02:00
27 changed files with 568 additions and 1153 deletions
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.17.0.cjs
yarnPath: .yarn/releases/yarn-4.16.0.cjs
+1 -28
View File
@@ -1,5 +1,4 @@
import { ContextProvider } from "@lit/context";
import type { PropertyValues, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
@@ -15,11 +14,6 @@ import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import {
configContext,
internationalizationContext,
} from "../../../../src/data/context";
import { updateHassGroups } from "../../../../src/data/context/updateContext";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
@@ -524,17 +518,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
private data = SCHEMAS.map(() => ({}));
// The date/datetime selectors and the date-picker dialog consume these
// contexts (provided by the root element in the real app). Provide them here
// so they work in the gallery.
private _i18nProvider = new ContextProvider(this, {
context: internationalizationContext,
});
private _configProvider = new ContextProvider(this, {
context: configContext,
});
constructor() {
super();
const hass = provideHass(this);
@@ -556,16 +539,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
el.hass = this.hass;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("hass") && this.hass) {
this._i18nProvider.setValue(
updateHassGroups.internationalization(this.hass)
);
this._configProvider.setValue(updateHassGroups.config(this.hass));
}
}
public connectedCallback() {
super.connectedCallback();
this.addEventListener("show-dialog", this._dialogManager);
+3 -3
View File
@@ -154,7 +154,7 @@
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@vitest/coverage-v8": "4.1.9",
"@vitest/coverage-v8": "4.1.8",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -197,7 +197,7 @@
"typescript": "6.0.3",
"typescript-eslint": "8.61.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.9",
"vitest": "4.1.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
@@ -213,7 +213,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.17.0",
"packageManager": "yarn@4.16.0",
"volta": {
"node": "24.17.0"
}
@@ -86,10 +86,7 @@ export class HaDateTimeSelector extends LitElement {
static styles = css`
.input {
display: flex;
/* Align the input fields by their top edge so the date field's underline
lines up with the time field, since ha-date-input reserves extra space
below for its hint while ha-time-input does not. */
align-items: flex-start;
align-items: center;
flex-direction: row;
}
+7 -8
View File
@@ -1,18 +1,15 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement {
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeEntityState({ entityIdPath: ["entityId"] })
private _stateObj?: HassEntity;
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@@ -39,7 +36,9 @@ class HaEntityMarker extends LitElement {
})}
></div>`
: this.showIcon && this.entityId
? html`<ha-state-icon .stateObj=${this._stateObj}></ha-state-icon>`
? html`<ha-state-icon
.stateObj=${this.hass?.states[this.entityId]}
></ha-state-icon>`
: !this.entityUnit
? this.entityName
: html`
@@ -128,6 +128,7 @@ export class HaLocationsEditor extends LitElement {
protected render(): TemplateResult {
return html`
<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
+33 -74
View File
@@ -1,6 +1,4 @@
import { consume } from "@lit/context";
import { isToday } from "date-fns";
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
import type {
Circle,
CircleMarker,
@@ -20,7 +18,6 @@ import {
formatTimeWeekday,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
@@ -29,22 +26,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import {
configContext,
connectionContext,
formattersContext,
internationalizationContext,
statesContext,
uiContext,
} from "../../data/context";
import type {
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantUI,
ThemeMode,
} from "../../types";
import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
@@ -94,32 +76,7 @@ export interface HaMapEntity {
@customElement("ha-map")
export class HaMap extends ReactiveElement {
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HassEntities;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config!: HassConfig;
@state()
@consume({ context: uiContext, subscribe: true })
private _ui!: HomeAssistantUI;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: HomeAssistantConnection;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
@@ -218,16 +175,17 @@ export class HaMap extends ReactiveElement {
return;
}
let autoFitRequired = false;
const oldStates = changedProps.get("_states") as HassEntities | undefined;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (changedProps.has("_loaded") || changedProps.has("entities")) {
this._drawEntities();
autoFitRequired = !this._pauseAutoFit;
} else if (this._loaded && oldStates && this.entities) {
} else if (this._loaded && oldHass && this.entities) {
// Check if any state has changed
for (const entity of this.entities) {
if (
oldStates[getEntityId(entity)] !== this._states[getEntityId(entity)]
oldHass.states[getEntityId(entity)] !==
this.hass!.states[getEntityId(entity)]
) {
this._drawEntities();
autoFitRequired = !this._pauseAutoFit;
@@ -261,11 +219,10 @@ export class HaMap extends ReactiveElement {
}, PROGRAMMITIC_FIT_DELAY);
}
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
if (
!changedProps.has("themeMode") &&
(!changedProps.has("_ui") ||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
(!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) {
return;
}
@@ -276,7 +233,7 @@ export class HaMap extends ReactiveElement {
private get _darkMode() {
return (
this.themeMode === "dark" ||
(this.themeMode === "auto" && Boolean(this._ui?.themes.darkMode))
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
);
}
@@ -301,8 +258,8 @@ export class HaMap extends ReactiveElement {
this._loading = true;
try {
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
latitude: this._config?.latitude ?? 52.3731339,
longitude: this._config?.longitude ?? 4.8903147,
latitude: this.hass?.config.latitude ?? 52.3731339,
longitude: this.hass?.config.longitude ?? 4.8903147,
zoom: this.zoom,
});
this._updateMapStyle();
@@ -343,7 +300,7 @@ export class HaMap extends ReactiveElement {
if (options?.unpause_autofit) {
this._pauseAutoFit = false;
}
if (!this.leafletMap || !this.Leaflet || !this._config) {
if (!this.leafletMap || !this.Leaflet || !this.hass) {
return;
}
@@ -354,7 +311,10 @@ export class HaMap extends ReactiveElement {
) {
this._isProgrammaticFit = true;
this.leafletMap.setView(
new this.Leaflet.LatLng(this._config.latitude, this._config.longitude),
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
options?.zoom || this.zoom
);
setTimeout(() => {
@@ -391,7 +351,7 @@ export class HaMap extends ReactiveElement {
boundingbox: LatLngExpression[],
options?: { zoom?: number; pad?: number }
) {
if (!this.leafletMap || !this.Leaflet) {
if (!this.leafletMap || !this.Leaflet || !this.hass) {
return;
}
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
@@ -422,31 +382,32 @@ export class HaMap extends ReactiveElement {
if (path.fullDatetime) {
formattedTime = formatDateTime(
point.timestamp,
this._i18n.locale,
this._config
this.hass.locale,
this.hass.config
);
} else if (isToday(point.timestamp)) {
formattedTime = formatTimeWithSeconds(
point.timestamp,
this._i18n.locale,
this._config
this.hass.locale,
this.hass.config
);
} else {
formattedTime = formatTimeWeekday(
point.timestamp,
this._i18n.locale,
this._config
this.hass.locale,
this.hass.config
);
}
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
}
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
// eslint-disable-next-line @typescript-eslint/naming-convention
const Leaflet = this.Leaflet;
if (!this._i18n || !this._config || !map || !Leaflet) {
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapPaths.length) {
@@ -574,12 +535,12 @@ export class HaMap extends ReactiveElement {
}
private _drawEntities(): void {
const states = this._states;
const hass = this.hass;
const map = this.leafletMap;
// eslint-disable-next-line @typescript-eslint/naming-convention
const Leaflet = this.Leaflet;
if (!states || !map || !Leaflet) {
if (!hass || !map || !Leaflet) {
return;
}
@@ -617,7 +578,7 @@ export class HaMap extends ReactiveElement {
const className = this._darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = states[getEntityId(entity)];
const stateObj = hass.states[getEntityId(entity)];
if (!stateObj) {
continue;
}
@@ -630,7 +591,7 @@ export class HaMap extends ReactiveElement {
entity_picture: entityPicture,
} = stateObj.attributes;
const location = getEntityLocation(stateObj, states);
const location = getEntityLocation(stateObj, hass.states);
if (!location) {
continue;
}
@@ -687,14 +648,11 @@ export class HaMap extends ReactiveElement {
// create icon
const entityName =
typeof entity !== "string" && entity.label_mode === "state"
? this._formatters.formatEntityState(stateObj)
? this.hass.formatEntityState(stateObj)
: typeof entity !== "string" &&
entity.label_mode === "attribute" &&
entity.attribute !== undefined
? this._formatters.formatEntityAttributeValue(
stateObj,
entity.attribute
)
? this.hass.formatEntityAttributeValue(stateObj, entity.attribute)
: (customTitle ??
title
.split(" ")
@@ -703,6 +661,7 @@ export class HaMap extends ReactiveElement {
.substr(0, 3));
const entityMarker = document.createElement("ha-entity-marker");
entityMarker.hass = this.hass;
entityMarker.showIcon =
typeof entity !== "string" && entity.label_mode === "icon";
entityMarker.entityId = getEntityId(entity);
@@ -715,7 +674,7 @@ export class HaMap extends ReactiveElement {
: "";
entityMarker.entityPicture =
entityPicture && (typeof entity === "string" || !entity.label_mode)
? this._connection.hassUrl(entityPicture)
? this.hass.hassUrl(entityPicture)
: "";
if (typeof entity !== "string") {
entityMarker.entityColor = entity.color;
+2 -2
View File
@@ -39,7 +39,6 @@ import {
mdiMicrophoneMessage,
mdiMotionSensor,
mdiPalette,
mdiRadioTower,
mdiRayVertex,
mdiRemote,
mdiRobot,
@@ -53,6 +52,7 @@ import {
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiVideoInputAntenna,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
@@ -129,7 +129,7 @@ export const FALLBACK_DOMAIN_ICONS = {
plant: mdiFlower,
power: mdiFlash,
proximity: mdiAppleSafari,
radio_frequency: mdiRadioTower,
radio_frequency: mdiVideoInputAntenna,
remote: mdiRemote,
scene: mdiPalette,
schedule: mdiCalendarClock,
-22
View File
@@ -1,22 +0,0 @@
import type { HomeAssistant } from "../types";
export const DOMAIN = "radio_frequency";
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",
});
+10 -12
View File
@@ -38,9 +38,8 @@ import {
} from "../common/const";
import { supportsFeature } from "../common/entity/supports-feature";
import { round } from "../common/number/round";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-svg-icon";
import type { HomeAssistant, HomeAssistantFormatters } from "../types";
import type { HomeAssistant } from "../types";
export enum WeatherEntityFeature {
FORECAST_DAILY = 1,
@@ -221,20 +220,19 @@ const getWindBearing = (bearing: number | string): string => {
};
export const getWind = (
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
localize: LocalizeFunc,
hass: HomeAssistant,
stateObj: WeatherEntity,
speed?: number,
bearing?: number | string
): string => {
const speedText =
speed !== undefined && speed !== null
? formatEntityAttributeValue(stateObj, "wind_speed", speed)
? hass.formatEntityAttributeValue(stateObj, "wind_speed", speed)
: "-";
if (bearing !== undefined && bearing !== null) {
const cardinalDirection = getWindBearing(bearing);
return `${speedText} (${
localize(
hass.localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection
})`;
@@ -280,13 +278,13 @@ export const getWeatherUnit = (
};
export const getSecondaryWeatherAttribute = (
hass: Pick<HomeAssistant, "formatEntityAttributeValue" | "localize">,
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
): TemplateResult | undefined => {
const extrema = getWeatherExtrema(
hass.formatEntityAttributeValue,
hass,
stateObj,
forecast,
temperatureFractionDigits
@@ -322,13 +320,13 @@ export const getSecondaryWeatherAttribute = (
? html`
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass.localize(`ui.card.weather.attributes.${attribute}`)}
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${hass.formatEntityAttributeValue(stateObj, attribute, roundedValue)}
`;
};
const getWeatherExtrema = (
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
@@ -371,11 +369,11 @@ const getWeatherExtrema = (
return html`
${tempHigh
? formatEntityAttributeValue(stateObj, "temperature", tempHigh)
? hass.formatEntityAttributeValue(stateObj, "temperature", tempHigh)
: ""}
${tempLow && tempHigh ? " / " : ""}
${tempLow
? formatEntityAttributeValue(stateObj, "temperature", tempLow)
? hass.formatEntityAttributeValue(stateObj, "temperature", tempLow)
: ""}
`;
};
@@ -1,46 +1,28 @@
import { consume } from "@lit/context";
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
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 memoizeOne from "memoize-one";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { getEntityLocation } from "../../../common/entity/get_entity_location";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-button";
import "../../../components/map/ha-map";
import { configContext, statesContext } from "../../../data/context";
import { showZoneEditor } from "../../../data/zone";
import type { CurrentUser, HomeAssistantConfig } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-person")
class MoreInfoPerson extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HassEntities;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, CurrentUser | undefined>({
transformer: ({ user }) => user,
})
private _user?: CurrentUser;
private _entityArray = memoizeOne((entityId: string) => [entityId]);
protected render() {
if (!this._localize || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
const location = getEntityLocation(this.stateObj, this._states);
const location = getEntityLocation(this.stateObj, this.hass.states);
const hasOwnCoordinates =
typeof this.stateObj.attributes.latitude === "number" &&
typeof this.stateObj.attributes.longitude === "number";
@@ -49,12 +31,13 @@ class MoreInfoPerson extends LitElement {
${location
? html`
<ha-map
.hass=${this.hass}
.entities=${this._entityArray(this.stateObj.entity_id)}
auto-fit
></ha-map>
`
: ""}
${!__DEMO__ && this._user?.is_admin && hasOwnCoordinates
${!__DEMO__ && this.hass.user?.is_admin && hasOwnCoordinates
? html`
<div class="actions">
<ha-button
@@ -62,7 +45,7 @@ class MoreInfoPerson extends LitElement {
size="s"
@click=${this._handleAction}
>
${this._localize(
${this.hass.localize(
"ui.dialogs.more_info_control.person.create_zone"
)}
</ha-button>
+14 -44
View File
@@ -1,42 +1,18 @@
import { consume } from "@lit/context";
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
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 { formatTime } from "../../../common/datetime/format_time";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-relative-time";
import {
configContext,
formattersContext,
internationalizationContext,
} from "../../../data/context";
import type {
HomeAssistantConfig,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-sun")
class MoreInfoSun extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config!: HassConfig;
protected render() {
if (!this._i18n || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -52,10 +28,10 @@ class MoreInfoSun extends LitElement {
<div class="key">
<span
>${item === "ris"
? this._i18n.localize(
? this.hass.localize(
"ui.dialogs.more_info_control.sun.rising"
)
: this._i18n.localize(
: this.hass.localize(
"ui.dialogs.more_info_control.sun.setting"
)}</span
>
@@ -66,8 +42,8 @@ class MoreInfoSun extends LitElement {
<div class="value">
${formatTime(
item === "ris" ? risingDate : settingDate,
this._i18n.locale,
this._config
this.hass.locale,
this.hass.config
)}
</div>
</div>
@@ -75,24 +51,18 @@ class MoreInfoSun extends LitElement {
)}
<div class="row">
<div class="key">
${this._i18n.localize("ui.dialogs.more_info_control.sun.elevation")}
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div>
<div class="value">
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"elevation"
)}
${this.hass.formatEntityAttributeValue(this.stateObj, "elevation")}
</div>
</div>
<div class="row">
<div class="key">
${this._i18n.localize("ui.dialogs.more_info_control.sun.azimuth")}
${this.hass.localize("ui.dialogs.more_info_control.sun.azimuth")}
</div>
<div class="value">
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"azimuth"
)}
${this.hass.formatEntityAttributeValue(this.stateObj, "azimuth")}
</div>
</div>
`;
@@ -1,6 +1,4 @@
import { consume } from "@lit/context";
import { mdiEye, mdiGauge, mdiWaterPercent, mdiWeatherWindy } from "@mdi/js";
import type { HassConfig } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -9,7 +7,6 @@ import memoizeOne from "memoize-one";
import { DragScrollController } from "../../../common/controllers/drag-scroll-controller";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { transform } from "../../../common/decorators/transform";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-alert";
import "../../../components/ha-relative-time";
@@ -19,12 +16,6 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-tooltip";
import {
configContext,
connectionContext,
formattersContext,
internationalizationContext,
} from "../../../data/context";
import type {
ForecastAttribute,
ForecastEvent,
@@ -42,36 +33,14 @@ import {
subscribeForecast,
weatherSVGStyles,
} from "../../../data/weather";
import type {
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-weather")
class MoreInfoWeather extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: WeatherEntity;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: HomeAssistantFormatters;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config!: HassConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: HomeAssistantConnection;
@state() private _forecastEvent?: ForecastEvent;
@state() private _forecastType?: ModernForecastType;
@@ -95,7 +64,7 @@ class MoreInfoWeather extends LitElement {
this._unsubscribeForecastEvents();
if (
!this.isConnected ||
!this._connection ||
!this.hass ||
!this.stateObj ||
!this._forecastType
) {
@@ -103,8 +72,8 @@ class MoreInfoWeather extends LitElement {
}
this._subscribed = subscribeForecast(
this._connection.connection,
this.stateObj.entity_id,
this.hass!.connection,
this.stateObj!.entity_id,
this._forecastType,
(event) => {
this._forecastEvent = event;
@@ -124,6 +93,23 @@ class MoreInfoWeather extends LitElement {
this._unsubscribeForecastEvents();
}
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
if (changedProps.has("stateObj")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!oldHass ||
oldHass.locale !== this.hass.locale ||
oldHass.config.unit_system !== this.hass.config.unit_system
) {
return true;
}
return false;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
@@ -180,7 +166,7 @@ class MoreInfoWeather extends LitElement {
});
protected render() {
if (!this._i18n || !this._formatters || !this._config || !this.stateObj) {
if (!this.hass || !this.stateObj) {
return nothing;
}
@@ -210,7 +196,7 @@ class MoreInfoWeather extends LitElement {
<div class="info">
<div class="name-state">
<div class="state">
${this._formatters.formatEntityState(this.stateObj)}
${this.hass.formatEntityState(this.stateObj)}
</div>
<div class="time-ago">
<ha-relative-time
@@ -221,7 +207,7 @@ class MoreInfoWeather extends LitElement {
<ha-tooltip for="relative-time">
<div class="row">
<span class="column-name">
${this._i18n.localize(
${this.hass.localize(
"ui.dialogs.more_info_control.last_changed"
)}:
</span>
@@ -232,7 +218,7 @@ class MoreInfoWeather extends LitElement {
</div>
<div class="row">
<span>
${this._i18n.localize(
${this.hass.localize(
"ui.dialogs.more_info_control.last_updated"
)}:
</span>
@@ -251,10 +237,10 @@ class MoreInfoWeather extends LitElement {
? html`
${formatNumber(
this.stateObj.attributes.temperature,
this._i18n.locale
this.hass.locale
)}&nbsp;<span
>${getWeatherUnit(
this._config,
this.hass.config,
this.stateObj,
"temperature"
)}</span
@@ -264,11 +250,7 @@ class MoreInfoWeather extends LitElement {
</div>
<div class="attribute">
${getSecondaryWeatherAttribute(
{
formatEntityAttributeValue:
this._formatters.formatEntityAttributeValue,
localize: this._i18n.localize,
},
this.hass,
this.stateObj,
forecast!
)}
@@ -281,12 +263,10 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
<ha-svg-icon .path=${mdiGauge}></ha-svg-icon>
<div class="main">
${this._i18n.localize(
"ui.card.weather.attributes.air_pressure"
)}
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div>
<div>
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"pressure"
)}
@@ -299,10 +279,10 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
<ha-svg-icon .path=${mdiWaterPercent}></ha-svg-icon>
<div class="main">
${this._i18n.localize("ui.card.weather.attributes.humidity")}
${this.hass.localize("ui.card.weather.attributes.humidity")}
</div>
<div>
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"humidity"
)}
@@ -315,12 +295,11 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
<ha-svg-icon .path=${mdiWeatherWindy}></ha-svg-icon>
<div class="main">
${this._i18n.localize("ui.card.weather.attributes.wind_speed")}
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
</div>
<div>
${getWind(
this._formatters.formatEntityAttributeValue,
this._i18n.localize,
this.hass,
this.stateObj,
this.stateObj.attributes.wind_speed!,
this.stateObj.attributes.wind_bearing
@@ -334,10 +313,10 @@ class MoreInfoWeather extends LitElement {
<div class="flex">
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
<div class="main">
${this._i18n.localize("ui.card.weather.attributes.visibility")}
${this.hass.localize("ui.card.weather.attributes.visibility")}
</div>
<div>
${this._formatters.formatEntityAttributeValue(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"visibility"
)}
@@ -348,7 +327,7 @@ class MoreInfoWeather extends LitElement {
${supportedForecasts?.length
? html`
<div class="section">
${this._i18n.localize("ui.card.weather.forecast")}:
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${supportedForecasts?.length > 1
? html`<ha-tab-group
@@ -361,7 +340,7 @@ class MoreInfoWeather extends LitElement {
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${this._i18n.localize(
${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
</ha-tab-group-tab>`
@@ -383,8 +362,8 @@ class MoreInfoWeather extends LitElement {
? html`<div class="forecast-day-header">
${formatDateWeekdayShort(
new Date(dayForecast[0].datetime),
this._i18n.locale,
this._config
this.hass!.locale,
this.hass!.config
)}
</div>`
: nothing}
@@ -402,23 +381,23 @@ class MoreInfoWeather extends LitElement {
${hourly
? formatTime(
new Date(item.datetime),
this._i18n.locale,
this._config
this.hass!.locale,
this.hass!.config
)
: dayNight
? html`<div class="daynight">
${item.is_daytime !== false
? this._i18n.localize(
? this.hass!.localize(
"ui.card.weather.day"
)
: this._i18n.localize(
: this.hass!.localize(
"ui.card.weather.night"
)}
</div>`
: formatDateWeekdayShort(
new Date(item.datetime),
this._i18n.locale,
this._config
this.hass!.locale,
this.hass!.config
)}
</div>
${this._showValue(item.condition)
@@ -439,7 +418,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this._i18n.locale
this.hass!.locale
)}°`
: "—"}
</div>
@@ -447,7 +426,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this._i18n.locale
this.hass!.locale
)}°`
: nothing}
</div>
@@ -136,6 +136,7 @@ class HaConfigSectionGeneral extends LitElement {
? html`
<div class="card-content">
<ha-map
.hass=${this.hass}
.entities=${["zone.home"]}
.zoom=${14}
.autoFit=${true}
@@ -9,6 +9,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-icon";
import "../../../../components/ha-list";
import "../../../../components/ha-list-item";
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import { entryIcon } from "../../../../data/icons";
import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog";
@@ -57,20 +58,29 @@ export class HaDeviceEntitiesCard extends LitElement {
}
});
// Main entities (those without their own name, shown as the device name)
// are listed first, separated from the additional entities by a divider.
const mainEntities: EntityRegistryStateEntry[] = [];
const additionalEntities: EntityRegistryStateEntry[] = [];
enabledEntities.forEach((entry) => {
if (computeEntityEntryName(entry, this.hass.devices)) {
additionalEntities.push(entry);
} else {
mainEntities.push(entry);
}
});
return html`
<ha-card outlined .header=${this.header}>
${enabledEntities.length
? html`
<div id="entities" class="move-up">
<ha-list>
${repeat(
enabledEntities,
(entry) => entry.entity_id,
(entry) =>
this.hass.states[entry.entity_id]
? this._renderEntity(entry)
: this._renderUnavailableEntity(entry)
)}
${this._renderEntities(mainEntities)}
${mainEntities.length && additionalEntities.length
? html`<div class="divider" role="separator"></div>`
: nothing}
${this._renderEntities(additionalEntities)}
</ha-list>
</div>
`
@@ -115,6 +125,17 @@ export class HaDeviceEntitiesCard extends LitElement {
this.showHidden = !this.showHidden;
}
private _renderEntities(entries: EntityRegistryStateEntry[]) {
return repeat(
entries,
(entry) => entry.entity_id,
(entry) =>
this.hass.states[entry.entity_id]
? this._renderEntity(entry)
: this._renderUnavailableEntity(entry)
);
}
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
let name = entry.stateName || this.deviceName;
if (entry.hidden_by) {
@@ -187,6 +208,11 @@ export class HaDeviceEntitiesCard extends LitElement {
.disabled-entry {
color: var(--secondary-text-color);
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 8px 16px;
}
.move-up {
margin-top: -13px;
}
-15
View File
@@ -20,7 +20,6 @@ import {
mdiPalette,
mdiPaletteSwatch,
mdiPuzzle,
mdiRadioTower,
mdiRemote,
mdiRobot,
mdiScrewdriver,
@@ -185,15 +184,6 @@ export const configSections: Record<string, PageNavigation[]> = {
adminOnly: true,
filter: getHasDomainCheck("infrared"),
},
{
path: "/config/radio-frequency",
iconPath: mdiRadioTower,
iconColor: "#E74011",
component: "radio_frequency",
translationKey: "radio_frequency",
adminOnly: true,
filter: getHasDomainCheck("radio_frequency"),
},
{
path: "/insteon",
iconPath:
@@ -695,11 +685,6 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),
},
"radio-frequency": {
tag: "radio-frequency-config-dashboard-router",
load: () =>
import("./integrations/integration-panels/radio_frequency/radio-frequency-config-dashboard-router"),
},
repairs: {
tag: "ha-config-repairs-dashboard",
load: () => import("./repairs/ha-config-repairs-dashboard"),
@@ -1,72 +0,0 @@
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { RadioFrequencyTransmitter } from "../../../../../data/radio_frequency";
import { fetchRadioFrequencyTransmitters } from "../../../../../data/radio_frequency";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../../../types";
@customElement("radio-frequency-config-dashboard-router")
class RadioFrequencyConfigDashboardRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@state() private _transmitters: RadioFrequencyTransmitter[] = [];
private _fetched = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "radio-frequency-config-dashboard",
load: () => import("./radio-frequency-config-dashboard"),
},
devices: {
tag: "radio-frequency-devices-page",
load: () => import("./radio-frequency-devices-page"),
},
},
};
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this._fetched && this.hass) {
this._fetched = true;
this._fetchTransmitters();
}
}
private async _fetchTransmitters(): Promise<void> {
try {
const result = await fetchRadioFrequencyTransmitters(this.hass);
this._transmitters = result.transmitters;
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.radio_frequency.loading_error"
),
text: err.message,
});
}
}
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.transmitters = this._transmitters;
}
}
declare global {
interface HTMLElementTagNameMap {
"radio-frequency-config-dashboard-router": RadioFrequencyConfigDashboardRouter;
}
}
@@ -1,197 +0,0 @@
import { mdiCheck, mdiCloseCircleOutline } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } 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 { UNAVAILABLE } from "../../../../../data/entity/entity";
import { FALLBACK_DOMAIN_ICONS } from "../../../../../data/icons";
import type { RadioFrequencyTransmitter } from "../../../../../data/radio_frequency";
import { DOMAIN } from "../../../../../data/radio_frequency";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
@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;
@property({ attribute: false })
public transmitters: RadioFrequencyTransmitter[] = [];
protected render(): TemplateResult {
const total = this.transmitters.length;
const online = this.transmitters.filter((transmitter) => {
const stateObj = this.hass.states[transmitter.entity_id];
return stateObj && stateObj.state !== UNAVAILABLE;
}).length;
const isOffline = online === 0;
const status = isOffline ? "offline" : "online";
const statusIcon = isOffline ? mdiCloseCircleOutline : mdiCheck;
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 class="network-status">
<div class="card-content">
<div class="heading">
<div class="icon ${status}">
<ha-svg-icon .path=${statusIcon}></ha-svg-icon>
</div>
<div class="details">
${this.hass.localize(
`ui.panel.config.radio_frequency.status_${status}`
)}<br />
<small>
${this.hass.localize(
"ui.panel.config.radio_frequency.devices_online_summary",
{ online, total }
)}
</small>
</div>
<ha-svg-icon
class="logo"
.path=${FALLBACK_DOMAIN_ICONS[DOMAIN]}
></ha-svg-icon>
</div>
</div>
</ha-card>
<ha-card class="network-card">
<div class="card-content">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/radio-frequency/devices"
>
<ha-svg-icon
slot="start"
.path=${FALLBACK_DOMAIN_ICONS[DOMAIN]}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.radio_frequency.devices_count",
{ count: total }
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
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-md-list {
background: none;
padding: 0;
}
.network-card {
overflow: hidden;
}
.network-card .card-content {
padding: 0;
}
.network-status div.heading {
display: flex;
align-items: center;
column-gap: var(--ha-space-4);
}
.network-status div.heading .logo {
margin-inline-start: auto;
--mdc-icon-size: 40px;
}
.network-status div.heading .icon {
position: relative;
border-radius: var(--ha-border-radius-2xl);
width: var(--ha-space-10);
height: var(--ha-space-10);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
--icon-color: var(--primary-color);
}
.network-status div.heading .icon.online {
--icon-color: var(--success-color);
}
.network-status div.heading .icon.offline {
--icon-color: var(--error-color);
}
.network-status div.heading .icon::before {
display: block;
content: "";
position: absolute;
inset: 0;
background-color: var(--icon-color, var(--primary-color));
opacity: 0.2;
}
.network-status div.heading .icon ha-svg-icon {
color: var(--icon-color, var(--primary-color));
width: var(--ha-space-6);
height: var(--ha-space-6);
}
.network-status div.heading .details {
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
color: var(--primary-text-color);
}
.network-status small {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
letter-spacing: 0.25px;
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"radio-frequency-config-dashboard": RadioFrequencyConfigDashboard;
}
}
@@ -1,152 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../../../../../common/navigate";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-relative-time";
import { UNAVAILABLE, UNKNOWN } from "../../../../../data/entity/entity";
import type { RadioFrequencyTransmitter } from "../../../../../data/radio_frequency";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
interface RadioFrequencyTransmitterRow {
id: string;
name: string;
type: string;
last_used?: string;
device_id: string | null;
}
@customElement("radio-frequency-devices-page")
export class RadioFrequencyDevicesPage 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;
@property({ attribute: false })
public transmitters: RadioFrequencyTransmitter[] = [];
private _tabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.radio_frequency.devices_navigation",
path: "/config/radio-frequency/devices",
},
];
private _columns = memoizeOne(
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<RadioFrequencyTransmitterRow> = {
name: {
title: localize("ui.panel.config.radio_frequency.name"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
flex: 2,
direction: "asc",
},
type: {
title: localize("ui.panel.config.radio_frequency.type"),
sortable: true,
filterable: true,
groupable: true,
},
last_used: {
title: localize("ui.panel.config.radio_frequency.last_used"),
sortable: true,
template: (transmitter) =>
transmitter.last_used
? html`<ha-relative-time
.hass=${this.hass}
.datetime=${transmitter.last_used}
capitalize
></ha-relative-time>`
: "—",
},
};
return columns;
}
);
private _data = memoizeOne(
(
transmitters: RadioFrequencyTransmitter[],
states: HomeAssistant["states"],
localize: LocalizeFunc
): RadioFrequencyTransmitterRow[] =>
transmitters.map((transmitter) => {
const stateObj = states[transmitter.entity_id];
// The entity state holds the timestamp the transmitter was last used
// (or unknown/unavailable when it never has been).
const state = stateObj?.state;
const last_used =
state &&
state !== UNAVAILABLE &&
state !== UNKNOWN &&
!isNaN(new Date(state).getTime())
? state
: undefined;
return {
id: transmitter.entity_id,
name: stateObj ? computeStateName(stateObj) : transmitter.entity_id,
type: localize("component.radio_frequency.entity_component._.name"),
last_used,
device_id: transmitter.device_id,
};
})
);
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${this._tabs}
back-path="/config/radio-frequency/dashboard"
.columns=${this._columns(this.hass.localize)}
.data=${this._data(
this.transmitters,
this.hass.states,
this.hass.localize
)}
.noDataText=${this.hass.localize(
"ui.panel.config.radio_frequency.no_devices"
)}
@row-click=${this._handleRowClicked}
clickable
></hass-tabs-subpage-data-table>
`;
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const transmitter = this.transmitters.find(
(t) => t.entity_id === ev.detail.id
);
if (transmitter?.device_id) {
navigate(`/config/devices/device/${transmitter.device_id}`);
}
}
static styles: CSSResultGroup = haStyle;
}
declare global {
interface HTMLElementTagNameMap {
"radio-frequency-devices-page": RadioFrequencyDevicesPage;
}
}
@@ -211,6 +211,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
<ha-card id="card" .header=${this._config.title}>
<div id="root">
<ha-map
.hass=${this.hass}
.entities=${this._filteredMapEntities}
.zoom=${this._config.default_zoom ?? DEFAULT_ZOOM}
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
@@ -378,8 +378,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${this._config.secondary_info_attribute ===
"wind_speed"
? getWind(
this.hass.formatEntityAttributeValue,
this.hass.localize,
this.hass,
stateObj,
stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing
@@ -32,7 +32,7 @@ import type { HomeAssistant } from "../../../../types";
import { computeShowHeaderToggle } from "../../cards/hui-entities-card";
import type { EntitiesCardConfig } from "../../cards/types";
import { processConfigEntities } from "../../common/process-config-entities";
import { timeFormatConfigStruct } from "../../components/types";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import type { LovelaceRowConfig } from "../../entity-rows/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
import type { LovelaceCardEditor } from "../../types";
@@ -119,7 +119,7 @@ const attributeEntitiesRowConfigStruct = object({
suffix: optional(string()),
name: optional(string()),
icon: optional(string()),
format: optional(timeFormatConfigStruct),
format: optional(enums(TIMESTAMP_RENDERING_FORMATS)),
});
const textEntitiesRowConfigStruct = object({
@@ -1,5 +1,5 @@
import { union, object, string, optional, boolean } from "superstruct";
import { timeFormatConfigStruct } from "../../components/types";
import { union, object, string, optional, boolean, enums } from "superstruct";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import {
actionConfigStruct,
actionConfigStructConfirmation,
@@ -13,7 +13,7 @@ export const entitiesConfigStruct = union([
icon: optional(string()),
image: optional(string()),
secondary_info: optional(string()),
format: optional(timeFormatConfigStruct),
format: optional(enums(TIMESTAMP_RENDERING_FORMATS)),
state_color: optional(boolean()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
+6 -1
View File
@@ -31,7 +31,12 @@ class HaPanelMap extends LitElement {
@click=${this._openZonesEditor}
></ha-icon-button>`
: ""}
<ha-map .entities=${this._entities} auto-fit interactive-zones></ha-map>
<ha-map
.hass=${this.hass}
.entities=${this._entities}
auto-fit
interactive-zones
></ha-map>
</ha-top-app-bar-fixed>
`;
}
-18
View File
@@ -1558,7 +1558,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%]",
@@ -2658,10 +2657,6 @@
"main": "Bluetooth",
"secondary": "Local device connectivity"
},
"radio_frequency": {
"main": "Radio frequency",
"secondary": "Control radio-based devices."
},
"knx": {
"main": "KNX",
"secondary": "Building automation standard"
@@ -7143,19 +7138,6 @@
"known_devices": "Known devices",
"unknown_devices": "Unknown devices"
},
"radio_frequency": {
"title": "Radio frequency",
"status_online": "Online",
"status_offline": "Offline",
"devices_online_summary": "{online}/{total} devices online",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"no_devices": "No radio frequency devices found",
"loading_error": "Failed to load radio frequency devices",
"name": "Name",
"type": "Type",
"last_used": "Last used",
"devices_navigation": "Devices"
},
"dhcp": {
"title": "DHCP discovery",
"mac_address": "MAC address",
+62 -62
View File
@@ -4926,12 +4926,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/coverage-v8@npm:4.1.9"
"@vitest/coverage-v8@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/coverage-v8@npm:4.1.8"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.1.9"
"@vitest/utils": "npm:4.1.8"
ast-v8-to-istanbul: "npm:^1.0.0"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
@@ -4941,34 +4941,34 @@ __metadata:
std-env: "npm:^4.0.0-rc.1"
tinyrainbow: "npm:^3.1.0"
peerDependencies:
"@vitest/browser": 4.1.9
vitest: 4.1.9
"@vitest/browser": 4.1.8
vitest: 4.1.8
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/1f236e17336973868aa6e7662b863b1c519d07840107daab3465429652741fc1f8a8988d1b7b28b8cfa883c7280f7479927a85e56c7874a0ddc5cc8ceda25cd6
checksum: 10/08d9ea65ca4cc007a1f1cdc85ea36d51bfa91a9b2f0e9ad27436b777629b4138e33dba2f68c8e68b01343310bf9d5624ad1d6d24553a5b289b66da51561259eb
languageName: node
linkType: hard
"@vitest/expect@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/expect@npm:4.1.9"
"@vitest/expect@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/expect@npm:4.1.8"
dependencies:
"@standard-schema/spec": "npm:^1.1.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/spy": "npm:4.1.8"
"@vitest/utils": "npm:4.1.8"
chai: "npm:^6.2.2"
tinyrainbow: "npm:^3.1.0"
checksum: 10/aba1a06cd28199f9c861d97797b014c0584fa6f6197e78345da0db5f74914d47f18958bb848658e889ca44452aa61e07ae851c16ea7b2175afd50d649dd4ed8c
checksum: 10/cb7d78e250ec77b7e180ac3e5f543501488c69b237d7ed97ffe9196c5e946b0e4a37be05a2ec38af7ce7750c1a98286480acdd247286a29c239b08a13b085d4b
languageName: node
linkType: hard
"@vitest/mocker@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/mocker@npm:4.1.9"
"@vitest/mocker@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/mocker@npm:4.1.8"
dependencies:
"@vitest/spy": "npm:4.1.9"
"@vitest/spy": "npm:4.1.8"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
peerDependencies:
@@ -4979,56 +4979,56 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/3e35ff3e2ecbdfbcae598e9c5c83978dd5f0cf3b16df37cf947c80faabce797ab275ca2075c3bb8ca85f595f3070267f93cb6798bbe415f1af2698f51833974c
checksum: 10/fc977703b07d950aa170bafdef988bc7ba88f0a80159d1563ce95696763729ec1f6d015012aad36cf4e1b522d327b205292c56d76692d2a9f72285d694ed3cba
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/pretty-format@npm:4.1.9"
"@vitest/pretty-format@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/pretty-format@npm:4.1.8"
dependencies:
tinyrainbow: "npm:^3.1.0"
checksum: 10/52512b300c000594c54bebbbfe31fab39e416a35d3686e2c46bc8e48ef8476d32306605f7736139608c3962943e0d22790dc15a3e6b1ffa436143d31f743a7c8
checksum: 10/56a4b685cdf9f2e9708025f17dab8c0fa990ab06e5b38606a1ddde52a09830a099843da6a1b127ee48217ab023bad7bd23c49eb4969d77dff07df363fad0bb0e
languageName: node
linkType: hard
"@vitest/runner@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/runner@npm:4.1.9"
"@vitest/runner@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/runner@npm:4.1.8"
dependencies:
"@vitest/utils": "npm:4.1.9"
"@vitest/utils": "npm:4.1.8"
pathe: "npm:^2.0.3"
checksum: 10/52e4e16e627faa62676f17683e570f505d58d2ce0ef421a3ae60e70c0ec5606d4af090fa6c7d5717d6e949f4401d6357b1f69cf06e52a5455a0ad9c9040268c0
checksum: 10/278d1482123877343731b3bb822d0280af928252ee263aab73ca189c39de3bb767ce715581870b2e1eb408f7cba01106a6989406cb2ada1332f181912558a3c1
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/snapshot@npm:4.1.9"
"@vitest/snapshot@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/snapshot@npm:4.1.8"
dependencies:
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.8"
"@vitest/utils": "npm:4.1.8"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10/c83349b1ad08d48284c1d3393168a7b7faffd24ace1ef337751a568dad322d83b0f9bc29378a4a60379cf2a13a268092b1d802936d6adb1ca28859f02dad8b87
checksum: 10/162ca0eccb72db02081b04307d21ac8d14c8fcd4a840872459274f589b1665f108bd4119dff19d5a2150a0e26b90531791ebec7ee74f0c2c5285b491cebbcfcb
languageName: node
linkType: hard
"@vitest/spy@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/spy@npm:4.1.9"
checksum: 10/8b8e42cc8e4b20d29bd8b312f34b9dbf2e20d4b4cdc24e3bcf6fd4d3b1f49e8924636d2730cca3946fbb45de893dfb531c77b832eb853c2624fdc2b800444e75
"@vitest/spy@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/spy@npm:4.1.8"
checksum: 10/53e948d8f5e229e969e704dc8a54fd42ad715b2b18f401592f4bba97dcf33bd4cf01d11af577d4efe42dc2d90c9e6574ec991531fd8f1bdfee916a1dd0828547
languageName: node
linkType: hard
"@vitest/utils@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/utils@npm:4.1.9"
"@vitest/utils@npm:4.1.8":
version: 4.1.8
resolution: "@vitest/utils@npm:4.1.8"
dependencies:
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.8"
convert-source-map: "npm:^2.0.0"
tinyrainbow: "npm:^3.1.0"
checksum: 10/78f5969fc09b1a95fda9dadd37e84a3a6ead35f66af15ad3b792eef35f80407047803e7afd53df86a8d794f59bf25ffbdc4146099140a3d5f9b51ea061bf2308
checksum: 10/13250b9e7825d425cc9a3d22aeb2e8d117c93e96a192138e93d76bfe7d5a391ab3888c5aa9e0394b0314bdff41e441ad7a32b0c0caa00cd202223b88087dcc78
languageName: node
linkType: hard
@@ -8506,7 +8506,7 @@ __metadata:
"@types/sortablejs": "npm:1.15.9"
"@types/tar": "npm:7.0.87"
"@vibrant/color": "npm:4.0.4"
"@vitest/coverage-v8": "npm:4.1.9"
"@vitest/coverage-v8": "npm:4.1.8"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
babel-loader: "npm:10.1.1"
@@ -8591,7 +8591,7 @@ __metadata:
typescript: "npm:6.0.3"
typescript-eslint: "npm:8.61.0"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.9"
vitest: "npm:4.1.8"
webpack-stats-plugin: "npm:1.1.3"
webpackbar: "npm:7.0.0"
weekstart: "npm:2.0.0"
@@ -13945,17 +13945,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.1.9":
version: 4.1.9
resolution: "vitest@npm:4.1.9"
"vitest@npm:4.1.8":
version: 4.1.8
resolution: "vitest@npm:4.1.8"
dependencies:
"@vitest/expect": "npm:4.1.9"
"@vitest/mocker": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/runner": "npm:4.1.9"
"@vitest/snapshot": "npm:4.1.9"
"@vitest/spy": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/expect": "npm:4.1.8"
"@vitest/mocker": "npm:4.1.8"
"@vitest/pretty-format": "npm:4.1.8"
"@vitest/runner": "npm:4.1.8"
"@vitest/snapshot": "npm:4.1.8"
"@vitest/spy": "npm:4.1.8"
"@vitest/utils": "npm:4.1.8"
es-module-lexer: "npm:^2.0.0"
expect-type: "npm:^1.3.0"
magic-string: "npm:^0.30.21"
@@ -13973,12 +13973,12 @@ __metadata:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.1.9
"@vitest/browser-preview": 4.1.9
"@vitest/browser-webdriverio": 4.1.9
"@vitest/coverage-istanbul": 4.1.9
"@vitest/coverage-v8": 4.1.9
"@vitest/ui": 4.1.9
"@vitest/browser-playwright": 4.1.8
"@vitest/browser-preview": 4.1.8
"@vitest/browser-webdriverio": 4.1.8
"@vitest/coverage-istanbul": 4.1.8
"@vitest/coverage-v8": 4.1.8
"@vitest/ui": 4.1.8
happy-dom: "*"
jsdom: "*"
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -14008,8 +14008,8 @@ __metadata:
vite:
optional: false
bin:
vitest: ./vitest.mjs
checksum: 10/64f9d1a0aae92c493c39822ecae8ec5b5a336fc27166f776d08c01ae79ef1ec5485a1826ef1451bb05df2edaae109894125c2ecceadaa56c17be2690f66f9758
vitest: vitest.mjs
checksum: 10/b9f1308436717da9558b36e149cac6bab8e3730aa7e90b49f9d7a84ba853e353d8afba7d406dc0abec731fb2a9ea9e92b89aba06b94b1a2802203048b43468af
languageName: node
linkType: hard