Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 261e08d0ed Restore page filter check in canShowPage 2026-06-06 16:25:49 +00:00
copilot-swe-agent[bot] 2763ed22b8 Align canShowPage with dev PageNavigation type 2026-06-06 15:42:12 +00:00
copilot-swe-agent[bot] 359b56e7f3 Merge origin/dev into rf-panel 2026-06-06 15:33:04 +00:00
Paulus Schoutsen fab4022dee Remove unused PageNavigation.not_component (#52463)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-06 11:37:55 +02:00
karwosts d70a42930b Fix helper no category message (#52462) 2026-06-06 11:35:09 +02:00
Paulus Schoutsen b91a087ab1 Fix oversized top-app-bar-fixed-adjust wrapper in gallery pages (#52467) 2026-06-06 08:00:39 +02:00
Paulus Schoutsen 0751aa0b66 Tweaks 2026-06-05 22:20:20 -04:00
Paulus Schoutsen 37100069ac Add rf panel 2026-06-03 21:12:28 +02:00
12 changed files with 303 additions and 109 deletions
+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 -10
View File
@@ -5,7 +5,7 @@ import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
isNotLoadedIntegration(hass, page);
(!page.filter || page.filter(hass));
export const isLoadedIntegration = (
hass: HomeAssistant,
@@ -16,13 +16,4 @@ 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;
@@ -32,10 +32,10 @@ export class HaAutomationRowLiveTest extends LitElement {
static styles = css`
:host {
display: inline-flex;
align-items: center;
vertical-align: middle;
margin-inline-start: var(--ha-space-1);
position: absolute;
top: -5px;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 10px;
+20
View File
@@ -0,0 +1,20 @@
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",
});
+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;
@@ -217,11 +217,19 @@ export default class HaAutomationConditionRow extends LitElement {
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
${this.optionsInSidebar && this.condition.condition !== "trigger"
? html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
</div>
${this.optionsInSidebar &&
this.condition.condition !== "trigger" &&
this._liveTestResult.message
? html`<ha-tooltip for="condition-live-test" slot="leading-icon"
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
@@ -237,15 +245,6 @@ export default class HaAutomationConditionRow extends LitElement {
this.condition.condition !== "device"
)
: nothing}
${this.optionsInSidebar && this.condition.condition !== "trigger"
? html`<ha-automation-row-live-test
id="condition-live-test"
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
${this.condition.note?.trim()
? html`
<ha-svg-icon
+24
View File
@@ -29,12 +29,14 @@ 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";
@@ -55,6 +57,14 @@ 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: [
{
@@ -166,6 +176,15 @@ 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:
@@ -667,6 +686,11 @@ 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.automation.picker.no_category_support"
"ui.panel.config.helpers.picker.no_category_support"
),
text: this.hass.localize(
"ui.panel.config.automation.picker.no_category_entity_reg"
"ui.panel.config.helpers.picker.no_category_entity_reg"
),
});
return;
@@ -0,0 +1,171 @@
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;
}
}
@@ -239,9 +239,17 @@ export class HaCardConditionEditor extends LitElement {
<ha-svg-icon
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
${hideLiveTest
? nothing
: html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`}
</div>
${!hideLiveTest && this._liveTestResult.message
? html`<ha-tooltip for="condition-live-test" slot="leading-icon"
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
@@ -249,15 +257,6 @@ export class HaCardConditionEditor extends LitElement {
${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
) || condition.condition}
${!hideLiveTest
? html`<ha-automation-row-live-test
id="condition-live-test"
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
</h3>
<ha-automation-row-event-chip
.show=${this._testingResult !== undefined}
+14 -1
View File
@@ -1616,6 +1616,7 @@
"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%]",
@@ -2718,6 +2719,10 @@
"main": "Bluetooth",
"secondary": "Local device connectivity"
},
"radio_frequency": {
"main": "Radio frequency",
"secondary": "Control radio-based devices."
},
"knx": {
"main": "KNX",
"secondary": "Building automation standard"
@@ -4389,7 +4394,9 @@
"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"
"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."
},
"dialog": {
"create": "Create",
@@ -7185,6 +7192,12 @@
"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,7 +2,6 @@ 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";
@@ -50,28 +49,6 @@ 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;