mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-11 17:57:33 +00:00
Compare commits
10 Commits
clock-date
...
bluetooth_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c492e84e58 | ||
|
|
e39ab6dfe4 | ||
|
|
76f43676d5 | ||
|
|
b84a51235d | ||
|
|
602d6a2337 | ||
|
|
6e614cd3f2 | ||
|
|
6793edd68b | ||
|
|
ad6e3267c3 | ||
|
|
f941117ca4 | ||
|
|
aef0bf03e3 |
@@ -141,6 +141,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
@@ -177,9 +178,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
entitiesOnly?: boolean,
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticComboBoxItem[] => {
|
||||
): StatisticComboBoxItem[] | undefined => {
|
||||
if (!statisticIds) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (includeStatisticsUnitOfMeasurement) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@@ -109,7 +109,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => PickerComboBoxItem[];
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -295,7 +295,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||
let items = [...(this.getItems(this._search, this.selectedSection) || [])];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) => {
|
||||
|
||||
@@ -54,7 +54,8 @@ export class HaChooseSelector extends LitElement {
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key
|
||||
this.selector.choose.translation_key,
|
||||
this.hass.localize
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -72,7 +73,11 @@ export class HaChooseSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne(
|
||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: HomeAssistant["localize"]
|
||||
) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
this.localizeValue && translationKey
|
||||
|
||||
@@ -2,13 +2,16 @@ import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { StateSelector } from "../../data/selector";
|
||||
import { extractFromTarget } from "../../data/target";
|
||||
import {
|
||||
resolveEntityIDs,
|
||||
type StateSelector,
|
||||
type TargetSelector,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
import "../entity/ha-entity-states-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-selector-state")
|
||||
export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@@ -30,6 +33,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
filter_attribute?: string;
|
||||
filter_entity?: string | string[];
|
||||
filter_target?: HassServiceTarget;
|
||||
target_selector?: TargetSelector;
|
||||
};
|
||||
|
||||
@state() private _entityIds?: string | string[];
|
||||
@@ -54,7 +58,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
this._resolveEntityIds(
|
||||
this.selector.state?.entity_id,
|
||||
this.context?.filter_entity,
|
||||
this.context?.filter_target
|
||||
this.context?.filter_target,
|
||||
this.context?.target_selector
|
||||
).then((entityIds) => {
|
||||
this._entityIds = entityIds;
|
||||
});
|
||||
@@ -104,7 +109,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
private async _resolveEntityIds(
|
||||
selectorEntityId: string | string[] | undefined,
|
||||
contextFilterEntity: string | string[] | undefined,
|
||||
contextFilterTarget: HassServiceTarget | undefined
|
||||
contextFilterTarget: HassServiceTarget | undefined,
|
||||
contextTargetSelector: TargetSelector | undefined
|
||||
): Promise<string | string[] | undefined> {
|
||||
if (selectorEntityId !== undefined) {
|
||||
return selectorEntityId;
|
||||
@@ -113,8 +119,14 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return contextFilterEntity;
|
||||
}
|
||||
if (contextFilterTarget !== undefined) {
|
||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
||||
return result.referenced_entities;
|
||||
return resolveEntityIDs(
|
||||
this.hass,
|
||||
contextFilterTarget,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
contextTargetSelector
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -185,21 +184,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
@@ -271,6 +271,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
|
||||
@@ -929,13 +929,13 @@ export const resolveEntityIDs = (
|
||||
targetPickerValue: HassServiceTarget,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"]
|
||||
areas: HomeAssistant["areas"],
|
||||
targetSelector: TargetSelector = { target: {} }
|
||||
): string[] => {
|
||||
if (!targetPickerValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetSelector = { target: {} };
|
||||
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
|
||||
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
|
||||
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
@@ -269,8 +268,11 @@ export class HaPlatformCondition extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
const context: Record<string, any> = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
if (data_key === "target" && this.description?.target) {
|
||||
context.target_selector = this._targetSelector(this.description.target);
|
||||
}
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.condition.target
|
||||
@@ -378,7 +380,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
`component.${getConditionDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import "../ha-config-section";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import {
|
||||
type EntityRegistryUpdate,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
@@ -305,8 +304,11 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
const context: Record<string, any> = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
if (data_key === "target" && this.description?.target) {
|
||||
context.target_selector = this._targetSelector(this.description.target);
|
||||
}
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.trigger.target
|
||||
@@ -414,7 +416,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
|
||||
`component.${getTriggerDomain(this.trigger.trigger)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
mdiScrewdriver,
|
||||
mdiScriptText,
|
||||
mdiShape,
|
||||
mdiLan,
|
||||
mdiSofa,
|
||||
mdiTools,
|
||||
mdiUpdate,
|
||||
@@ -126,7 +125,8 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
{
|
||||
path: "/knx",
|
||||
name: "KNX",
|
||||
iconPath: mdiLan,
|
||||
iconPath:
|
||||
"M 3.9861338,14.261456 3.7267552,13.934877 6.3179131,11.306266 H 4.466374 l -2.6385205,2.68258 V 11.312882 H 0.00440574 L 0,17.679803 l 1.8278535,5.43e-4 v -1.818482 l 0.7225444,-0.732459 2.1373588,2.543782 2.1869324,-5.44e-4 M 24,17.680369 21.809238,17.669359 19.885559,15.375598 17.640262,17.68037 h -1.828407 l 3.236048,-3.302138 -2.574075,-3.067547 2.135161,0.0016 1.610309,1.87687 1.866403,-1.87687 h 1.828429 l -2.857742,2.87478 m -10.589867,-2.924898 2.829625,3.990552 -0.01489,-3.977887 1.811889,-0.0044 0.0011,6.357564 -2.093314,-5.44e-4 -2.922133,-3.947594 -0.0314,3.947594 H 8.2581097 V 11.261677 M 11.971203,6.3517488 c 0,0 2.800714,-0.093203 6.172001,1.0812045 3.462393,1.0898845 5.770926,3.4695627 5.770926,3.4695627 l -1.823898,-5.43e-4 C 22.088532,10.900273 20.577938,9.4271528 17.660223,8.5024618 15.139256,7.703366 12.723057,7.645835 12.111178,7.6449876 l -9.71e-4,0.0011 c 0,0 -0.0259,-6.4e-4 -0.07527,-9.714e-4 -0.04726,3.33e-4 -0.07201,9.714e-4 -0.07201,9.714e-4 v -0.00113 C 11.337007,7.6453728 8.8132091,7.7001736 6.2821829,8.5024618 3.3627914,9.4276738 1.8521646,10.901973 1.8521646,10.901973 l -1.82398708,5.43e-4 C 0.03128403,10.899322 2.339143,8.5221038 5.799224,7.4329533 9.170444,6.2585642 11.971203,6.3517488 11.971203,6.3517488 Z",
|
||||
iconColor: "#4EAA66",
|
||||
component: "knx",
|
||||
translationKey: "knx",
|
||||
@@ -135,10 +135,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
path: "/config/thread",
|
||||
name: "Thread",
|
||||
iconPath:
|
||||
"M82.498,0C37.008,0,0,37.008,0,82.496c0,45.181,36.51,81.977,81.573,82.476V82.569l-27.002-0.002 c-8.023,0-14.554,6.53-14.554,14.561c0,8.023,6.531,14.551,14.554,14.551v17.98c-17.939,0-32.534-14.595-32.534-32.531 c0-17.944,14.595-32.543,32.534-32.543l27.002,0.004v-9.096c0-14.932,12.146-27.08,27.075-27.08 c14.932,0,27.082,12.148,27.082,27.08c0,14.931-12.15,27.08-27.082,27.08l-9.097-0.001v80.641 C136.889,155.333,165,122.14,165,82.496C165,37.008,127.99,0,82.498,0z",
|
||||
iconSecondaryPath:
|
||||
"M117.748 55.493C117.748 50.477 113.666 46.395 108.648 46.395C103.633 46.395 99.551 50.477 99.551 55.493V64.59L108.648 64.591C113.666 64.591 117.748 60.51 117.748 55.493Z",
|
||||
iconViewBox: "0 0 165 165",
|
||||
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z",
|
||||
iconColor: "#ED7744",
|
||||
component: "thread",
|
||||
translationKey: "thread",
|
||||
@@ -155,8 +152,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
path: "/insteon",
|
||||
name: "Insteon",
|
||||
iconPath:
|
||||
"M82.5108 43.8917H82.7152C107.824 43.8917 129.241 28.1166 137.629 5.95738L105.802 0L82.5108 43.8917ZM82.5108 43.8917H82.3065C57.1975 43.8917 35.7811 28.1352 27.3928 5.95738H27.3742L59.2015 0L82.5108 43.8917ZM43.8903 82.4951V82.2908C43.8903 57.1805 28.1158 35.7636 5.95718 27.3751L0 59.2037L43.8903 82.4951ZM43.8903 82.4951V82.6989C43.8903 107.809 28.1343 129.226 5.95718 137.615V137.633L0 105.805L43.8903 82.4951ZM165 59.2037L159.043 27.3751V27.3936C136.865 35.7822 121.11 57.1991 121.11 82.3094V82.5133V82.7176V82.7363C121.11 107.846 136.884 129.263 159.043 137.652L165 105.823L121.11 82.5133L165 59.2037ZM137.628 159.043L105.8 165L82.4912 121.108H82.695C107.804 121.108 129.221 136.865 137.609 159.043H137.628ZM82.4912 121.108L59.1818 165L27.3545 159.043C35.7428 136.884 57.1592 121.108 82.2682 121.108H82.2868H82.4912Z",
|
||||
iconViewBox: "0 0 165 165",
|
||||
"m 12.001571,6.3842473 h 0.02973 c 3.652189,0 6.767389,-2.29456 7.987462,-5.5177193 L 15.389382,0 Z m 0,0 h -0.02972 c -3.6522186,0 -6.7673314,-2.2918546 -7.9874477,-5.5177193 h -0.00271 L 8.6111273,0 Z M 6.3840436,11.999287 v -0.02972 c 0,-3.6524074 -2.2944727,-6.7675928 -5.51754469,-7.9877383 L 0,8.6114473 Z m 0,0 v 0.02964 c 0,3.652378 -2.2917818,6.767578 -5.51754469,7.987796 v 0.0026 L 0,15.389818 Z M 24,8.6114473 23.133527,3.9818327 v 0.00269 C 19.907636,5.2046836 17.616,8.3198691 17.616,11.972276 v 0.02966 0.02972 0.0027 c 0,3.65232 2.2944,6.76752 5.517527,7.987738 L 24,15.392436 17.616,12.001935 Z M 20.018618,23.133527 15.389091,24 11.99872,17.615709 h 0.02964 c 3.652218,0 6.767418,2.291927 7.987491,5.517818 z M 11.99872,17.615709 8.6082618,24 3.9788364,23.133527 C 5.1989527,19.9104 8.3140655,17.615709 11.966284,17.615709 h 0.0027 z",
|
||||
iconColor: "#E4002C",
|
||||
component: "insteon",
|
||||
translationKey: "insteon",
|
||||
|
||||
@@ -23,23 +23,12 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
||||
|
||||
export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.advertisement_monitor",
|
||||
path: "advertisement-monitor",
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.visualization",
|
||||
path: "visualization",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-advertisement-monitor")
|
||||
export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -232,7 +221,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
filter=${this.address || ""}
|
||||
clickable
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
></hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/ha-button";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
mdiBroadcast,
|
||||
mdiCogOutline,
|
||||
mdiLan,
|
||||
mdiLinkVariant,
|
||||
mdiNetwork,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothScannerState,
|
||||
@@ -23,29 +21,60 @@ import type {
|
||||
HaScannerType,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../../../util/calculate";
|
||||
import "../../../../../components/ha-metric";
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
export const bluetoothTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.overview",
|
||||
path: `/config/bluetooth/dashboard`,
|
||||
iconPath: mdiNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.advertisements",
|
||||
path: `/config/bluetooth/advertisement-monitor`,
|
||||
iconPath: mdiBroadcast,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.visualization",
|
||||
path: `/config/bluetooth/visualization`,
|
||||
iconPath: mdiLan,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.connections",
|
||||
path: `/config/bluetooth/connection-monitor`,
|
||||
iconPath: mdiLinkVariant,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-config-dashboard")
|
||||
export class BluetoothConfigDashboard 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 _configEntries: ConfigEntry[] = [];
|
||||
|
||||
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
|
||||
|
||||
@state() private _connectionAllocationsError?: string;
|
||||
|
||||
@state() private _scannerState?: BluetoothScannerState;
|
||||
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
|
||||
|
||||
@state() private _scannerDetails?: BluetoothScannersDetails;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
private _unsubConnectionAllocations?: (() => Promise<void>) | undefined;
|
||||
|
||||
private _unsubScannerState?: (() => Promise<void>) | undefined;
|
||||
@@ -55,41 +84,44 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass) {
|
||||
this._loadConfigEntries();
|
||||
this._subscribeBluetoothConnectionAllocations();
|
||||
this._subscribeBluetoothScannerState();
|
||||
this._subscribeScannerDetails();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadConfigEntries(): Promise<void> {
|
||||
this._configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
});
|
||||
}
|
||||
|
||||
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
|
||||
if (this._unsubConnectionAllocations || !this._configEntry) {
|
||||
if (this._unsubConnectionAllocations) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
},
|
||||
this._configEntry
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._unsubConnectionAllocations = undefined;
|
||||
this._connectionAllocationsError = err.message;
|
||||
}
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _subscribeBluetoothScannerState(): Promise<void> {
|
||||
if (this._unsubScannerState || !this._configEntry) {
|
||||
if (this._unsubScannerState) {
|
||||
return;
|
||||
}
|
||||
this._unsubScannerState = await subscribeBluetoothScannerState(
|
||||
this.hass.connection,
|
||||
(scannerState) => {
|
||||
this._scannerState = scannerState;
|
||||
},
|
||||
this._configEntry
|
||||
this._scannerStates = {
|
||||
...this._scannerStates,
|
||||
[scannerState.source]: scannerState,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,92 +154,111 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
// Get scanner type to determine if options button should be shown
|
||||
const scannerDetails =
|
||||
this._scannerState && this._scannerDetails?.[this._scannerState.source];
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
const isRemoteScanner = scannerType === "remote";
|
||||
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<hass-tabs-subpage
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.title")}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.settings_title"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">${this._renderScannerState()}</div>
|
||||
${!isRemoteScanner
|
||||
? html`<div class="card-actions">
|
||||
<ha-button @click=${this._openOptionFlow}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.option_flow"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor_details"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/advertisement-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
href="/config/bluetooth/visualization"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._renderConnectionAllocations()}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/connection-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-list>${this._renderAdaptersList()}</ha-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getUsedAllocations = (used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total));
|
||||
private _renderAdaptersList() {
|
||||
if (this._configEntries.length === 0) {
|
||||
return html`<ha-list-item noninteractive>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</ha-list-item>`;
|
||||
}
|
||||
|
||||
// Build source to device mapping (same as visualization)
|
||||
const sourceDevices: Record<string, DeviceRegistryEntry> = {};
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
const btConnection = device.connections.find(
|
||||
(connection) => connection[0] === "bluetooth"
|
||||
);
|
||||
if (btConnection) {
|
||||
sourceDevices[btConnection[1]] = device;
|
||||
}
|
||||
});
|
||||
|
||||
return this._configEntries.map((entry) => {
|
||||
// Find scanner by matching device's config_entries to this entry
|
||||
const scannerDetails = this._scannerDetails
|
||||
? Object.values(this._scannerDetails).find((d) => {
|
||||
const device = sourceDevices[d.source];
|
||||
return device?.config_entries.includes(entry.entry_id);
|
||||
})
|
||||
: undefined;
|
||||
const scannerState = scannerDetails
|
||||
? this._scannerStates[scannerDetails.source]
|
||||
: undefined;
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
const isRemoteScanner = scannerType === "remote";
|
||||
const hasMismatch =
|
||||
scannerState &&
|
||||
scannerState.current_mode !== scannerState.requested_mode;
|
||||
|
||||
// Find allocation data for this scanner
|
||||
const allocations = scannerDetails
|
||||
? this._connectionAllocationData.find(
|
||||
(a) => a.source === scannerDetails.source
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const secondaryText = this._formatScannerModeText(scannerState);
|
||||
|
||||
return html`
|
||||
<ha-list-item twoline hasMeta noninteractive>
|
||||
<span>${entry.title}</span>
|
||||
<span slot="secondary">
|
||||
${secondaryText}${allocations
|
||||
? allocations.slots > 0
|
||||
? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}`
|
||||
: ` · ${this.hass.localize("ui.panel.config.bluetooth.no_connection_slots")}`
|
||||
: nothing}
|
||||
</span>
|
||||
${!isRemoteScanner
|
||||
? html`<ha-icon-button
|
||||
slot="meta"
|
||||
.path=${mdiCogOutline}
|
||||
.entry=${entry}
|
||||
@click=${this._openOptionFlow}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.option_flow"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
${hasMismatch && scannerDetails
|
||||
? this._renderScannerMismatchWarning(
|
||||
entry.title,
|
||||
scannerState,
|
||||
scannerType
|
||||
)
|
||||
: nothing}
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private _renderScannerMismatchWarning(
|
||||
name: string,
|
||||
scannerState: BluetoothScannerState,
|
||||
scannerType: HaScannerType,
|
||||
formatMode: (mode: string | null) => string
|
||||
scannerType: HaScannerType
|
||||
) {
|
||||
const instructions: string[] = [];
|
||||
|
||||
@@ -238,8 +289,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_mode_mismatch",
|
||||
{
|
||||
requested: formatMode(scannerState.requested_mode),
|
||||
current: formatMode(scannerState.current_mode),
|
||||
name: name,
|
||||
requested: this._formatMode(scannerState.requested_mode),
|
||||
current: this._formatMode(scannerState.current_mode),
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
@@ -249,127 +301,59 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
private _renderScannerState() {
|
||||
if (!this._configEntry || !this._scannerState) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</div>`;
|
||||
private _formatMode(mode: string | null): string {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive"
|
||||
);
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
|
||||
const scannerState = this._scannerState;
|
||||
// Find the scanner details for this source
|
||||
const scannerDetails = this._scannerDetails?.[scannerState.source];
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
|
||||
const formatMode = (mode: string | null) => {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive"
|
||||
);
|
||||
default:
|
||||
return mode; // Fallback for unknown modes
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="scanner-state">
|
||||
<div class="state-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.current_scanning_mode"
|
||||
)}:</span
|
||||
>
|
||||
<span class="state-value"
|
||||
>${formatMode(scannerState.current_mode)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="state-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.requested_scanning_mode"
|
||||
)}:</span
|
||||
>
|
||||
<span class="state-value"
|
||||
>${formatMode(scannerState.requested_mode)}</span
|
||||
>
|
||||
</div>
|
||||
${scannerState.current_mode !== scannerState.requested_mode
|
||||
? this._renderScannerMismatchWarning(
|
||||
scannerState,
|
||||
scannerType,
|
||||
formatMode
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderConnectionAllocations() {
|
||||
if (this._connectionAllocationsError) {
|
||||
return html`<ha-alert alert-type="error"
|
||||
>${this._connectionAllocationsError}</ha-alert
|
||||
>`;
|
||||
private _formatModeLabel(mode: string | null): string {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none_label"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active_label"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive_label"
|
||||
);
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
if (this._connectionAllocationData.length === 0) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_connection_slot_allocations"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
const allocations = this._connectionAllocationData[0];
|
||||
const allocationsUsed = allocations.slots - allocations.free;
|
||||
const allocationsTotal = allocations.slots;
|
||||
if (allocationsTotal === 0) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_active_connection_support"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
return html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_details",
|
||||
{ slots: allocationsTotal }
|
||||
)}
|
||||
</p>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.used_connection_slot_allocations"
|
||||
)}
|
||||
.value=${this._getUsedAllocations(allocationsUsed, allocationsTotal)}
|
||||
.tooltip=${allocations.allocated.length > 0
|
||||
? `${allocationsUsed}/${allocationsTotal} (${allocations.allocated.join(", ")})`
|
||||
: `${allocationsUsed}/${allocationsTotal}`}
|
||||
></ha-metric>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _openOptionFlow() {
|
||||
const configEntryId = this._configEntry;
|
||||
if (!configEntryId) {
|
||||
return;
|
||||
private _formatScannerModeText(
|
||||
scannerState: BluetoothScannerState | undefined
|
||||
): string {
|
||||
if (!scannerState) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_state_unknown"
|
||||
);
|
||||
}
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
});
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === configEntryId
|
||||
);
|
||||
showOptionsFlowDialog(this, configEntry!);
|
||||
|
||||
return this._formatModeLabel(scannerState.current_mode);
|
||||
}
|
||||
|
||||
private _openOptionFlow(ev: Event) {
|
||||
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
|
||||
showOptionsFlowDialog(this, button.entry);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -394,17 +378,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.scanner-state {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.state-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.state-value {
|
||||
font-weight: 500;
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-display: flex;
|
||||
--mdc-list-item-meta-size: 48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
@customElement("bluetooth-connection-monitor")
|
||||
export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
@@ -214,6 +215,7 @@ export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._dataWithNamedSourceAndIds(this._data)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
const UPDATE_THROTTLE_TIME = 10000;
|
||||
|
||||
@@ -123,8 +123,7 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
@@ -28,11 +26,6 @@ function romanize12HourClock(num: number) {
|
||||
return numerals[num];
|
||||
}
|
||||
|
||||
const INTERVAL = 1000;
|
||||
const QUARTER_TICKS = Array.from({ length: 4 }, (_, i) => i);
|
||||
const HOUR_TICKS = Array.from({ length: 12 }, (_, i) => i);
|
||||
const MINUTE_TICKS = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
@customElement("hui-clock-card-analog")
|
||||
export class HuiClockCardAnalog extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -47,49 +40,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
@state() private _year = "";
|
||||
|
||||
@state() private _month = "";
|
||||
|
||||
@state() private _day = "";
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _currentDate = new Date();
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeDateTime();
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._startTick();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeDateTime();
|
||||
}
|
||||
};
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
@@ -101,27 +51,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -131,31 +60,42 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._computeDateTime();
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tick();
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDate() {
|
||||
this._currentDate = new Date();
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _computeDateTime() {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
}
|
||||
};
|
||||
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
this._updateDate();
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts(this._currentDate);
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const hourStr = parts.find((p) => p.type === "hour")?.value;
|
||||
const minuteStr = parts.find((p) => p.type === "minute")?.value;
|
||||
const secondStr = parts.find((p) => p.type === "second")?.value;
|
||||
@@ -163,7 +103,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = this._currentDate.getMilliseconds();
|
||||
const ms = new Date().getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
const hour12 = hour % 12;
|
||||
@@ -171,36 +111,18 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
|
||||
// Also update date parts if date is shown
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._year = parts.find((p) => p.type === "year")?.value ?? "";
|
||||
this._month = parts.find((p) => p.type === "month")?.value ?? "";
|
||||
this._day = parts.find((p) => p.type === "day")?.value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
this._computeDateTime();
|
||||
}
|
||||
|
||||
private _computeClock = memoizeOne((config: ClockCardConfig) => {
|
||||
const faceParts = config.face_style?.split("_");
|
||||
|
||||
return {
|
||||
sizeClass: config.clock_size ? `size-${config.clock_size}` : "",
|
||||
isNumbers: faceParts?.includes("numbers") ?? false,
|
||||
isRoman: faceParts?.includes("roman") ?? false,
|
||||
isUpright: faceParts?.includes("upright") ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const { sizeClass, isNumbers, isRoman, isUpright } = this._computeClock(
|
||||
this.config
|
||||
);
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
const isNumbers = this.config?.face_style?.startsWith("numbers");
|
||||
const isRoman = this.config?.face_style?.startsWith("roman");
|
||||
const isUpright = this.config?.face_style?.endsWith("upright");
|
||||
|
||||
const indicator = (number?: number) => html`
|
||||
<div
|
||||
@@ -241,14 +163,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
})}
|
||||
>
|
||||
${this.config.ticks === "quarter"
|
||||
? QUARTER_TICKS.map(
|
||||
? Array.from({ length: 4 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 4 ticks (12, 3, 6, 9) at 0°, 90°, 180°, 270°
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 90}deg` })}
|
||||
style=${`--tick-rotation: ${i * 90}deg;`}
|
||||
>
|
||||
${indicator([12, 3, 6, 9][i])}
|
||||
</div>
|
||||
@@ -256,30 +178,28 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
)
|
||||
: !this.config.ticks || // Default to hour ticks
|
||||
this.config.ticks === "hour"
|
||||
? HOUR_TICKS.map(
|
||||
? Array.from({ length: 12 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 12 ticks (1-12)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 30}deg` })}
|
||||
style=${`--tick-rotation: ${i * 30}deg;`}
|
||||
>
|
||||
${indicator(((i + 11) % 12) + 1)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: this.config.ticks === "minute"
|
||||
? MINUTE_TICKS.map(
|
||||
? Array.from({ length: 60 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 60 ticks (1-60)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
|
||||
style=${styleMap({
|
||||
"--tick-rotation": `${i * 6}deg`,
|
||||
})}
|
||||
style=${`--tick-rotation: ${i * 6}deg;`}
|
||||
>
|
||||
${i % 5 === 0
|
||||
? indicator(((i / 5 + 11) % 12) + 1)
|
||||
@@ -288,27 +208,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.config?.date && this.config.date !== "none"
|
||||
? html`<div class="date-parts ${sizeClass}">
|
||||
<span class="date-part day-month"
|
||||
>${this._day} ${this._month}</span
|
||||
>
|
||||
<span class="date-part year">${this._year}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._hourOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._minuteOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
@@ -317,13 +224,11 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -502,41 +407,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.date-parts {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"day-month"
|
||||
"year";
|
||||
direction: ltr;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.date-parts.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date-parts.size-large {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
|
||||
.date-part.day-month {
|
||||
grid-area: day-month;
|
||||
}
|
||||
|
||||
.date-part.year {
|
||||
grid-area: year;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
@@ -41,27 +39,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
const h12 = useAmPm(locale);
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: h12 ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -116,16 +93,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
|
||||
this._date = this.config?.date
|
||||
? [
|
||||
parts.find((part) => part.type === "day")?.value,
|
||||
parts.find((part) => part.type === "month")?.value,
|
||||
parts.find((part) => part.type === "year")?.value,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
: undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -146,9 +113,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.config.date && this.config.date !== "none"
|
||||
? html`<div class="date ${sizeClass}">${this._date}</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -224,20 +188,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,6 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date?: "none" | "short" | "long" | "day" | "day-month" | "day-month-long";
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
|
||||
@@ -39,19 +39,6 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("short"),
|
||||
literal("long"),
|
||||
literal("day"),
|
||||
literal("day-month"),
|
||||
literal("day-month-long"),
|
||||
]),
|
||||
literal("none")
|
||||
)
|
||||
),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
@@ -106,7 +93,7 @@ export class HuiClockCardEditor
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
mode: "dropdown",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
@@ -132,27 +119,6 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "date",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: [
|
||||
"none",
|
||||
"short",
|
||||
"long",
|
||||
"day",
|
||||
"day-month",
|
||||
"day-month-long",
|
||||
].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.dates.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
@@ -294,14 +260,13 @@ export class HuiClockCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config: ClockCardConfig) => ({
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_zone: "auto",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
date: "none",
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
@@ -325,9 +290,8 @@ export class HuiClockCardEditor
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config)
|
||||
.clock_style as ClockCardConfig["clock_style"],
|
||||
this._data(this._config).ticks as ClockCardConfig["ticks"],
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -403,10 +367,6 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.label`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.label`
|
||||
@@ -432,10 +392,6 @@ export class HuiClockCardEditor
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.description`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
|
||||
@@ -6011,26 +6011,28 @@
|
||||
},
|
||||
"bluetooth": {
|
||||
"title": "Bluetooth",
|
||||
"settings_title": "Bluetooth settings",
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"advertisements": "Advertisements",
|
||||
"visualization": "Visualization",
|
||||
"connections": "Connections"
|
||||
},
|
||||
"settings_title": "Bluetooth adapters",
|
||||
"option_flow": "Configure Bluetooth options",
|
||||
"advertisement_monitor": "Advertisement monitor",
|
||||
"advertisement_monitor_details": "The advertisement monitor listens for Bluetooth advertisements and displays the data in a structured format.",
|
||||
"connection_slot_allocations_monitor": "Connection slot allocations monitor",
|
||||
"connection_slot_allocations_monitor_details": "The connection slot allocations monitor displays the (GATT) connection slot allocations for the adapter. This adapter supports up to {slots} simultaneous connections. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
|
||||
"connection_monitor": "Connection monitor",
|
||||
"visualization": "Visualization",
|
||||
"used_connection_slot_allocations": "Used connection slot allocations",
|
||||
"no_connections": "No active connections",
|
||||
"active_connections": "connections",
|
||||
"no_advertisements_found": "No matching Bluetooth advertisements found",
|
||||
"no_connection_slot_allocations": "No connection slot allocations information available",
|
||||
"no_active_connection_support": "This adapter does not support making active (GATT) connections.",
|
||||
"no_connection_slots": "No connection slots",
|
||||
"no_scanner_state_available": "No scanner state available",
|
||||
"current_scanning_mode": "Current scanning mode",
|
||||
"requested_scanning_mode": "Requested scanning mode",
|
||||
"scanner_state_unknown": "State unknown",
|
||||
"scanning_mode_none": "none",
|
||||
"scanning_mode_active": "active",
|
||||
"scanning_mode_passive": "passive",
|
||||
"scanner_mode_mismatch": "Scanner requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
|
||||
"scanning_mode_active_label": "Active scanning",
|
||||
"scanning_mode_passive_label": "Passive scanning",
|
||||
"scanning_mode_none_label": "No scanning",
|
||||
"scanner_mode_mismatch": "{name} requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
|
||||
"scanner_mode_mismatch_remote": "For proxies: reboot the device",
|
||||
"scanner_mode_mismatch_usb": "For USB adapters: unplug and plug back in",
|
||||
"scanner_mode_mismatch_uart": "For UART/onboard adapters: power down the system completely and power it back up",
|
||||
@@ -6048,7 +6050,6 @@
|
||||
"service_uuids": "Service UUIDs",
|
||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||
"area": "Area",
|
||||
"core": "Home Assistant",
|
||||
"scanners": "Scanners",
|
||||
"known_devices": "Known devices",
|
||||
"unknown_devices": "Unknown devices"
|
||||
@@ -8211,18 +8212,6 @@
|
||||
"large": "Large"
|
||||
},
|
||||
"show_seconds": "Display seconds",
|
||||
"date": {
|
||||
"label": "Date",
|
||||
"description": "Whether to show the date on the clock. Can also be a custom date format."
|
||||
},
|
||||
"dates": {
|
||||
"none": "No date",
|
||||
"short": "Short",
|
||||
"long": "Long",
|
||||
"day": "Day only",
|
||||
"day-month": "Day and month",
|
||||
"day-month-long": "Day and month (long)"
|
||||
},
|
||||
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
|
||||
"time_formats": {
|
||||
"auto": "Use user settings",
|
||||
|
||||
Reference in New Issue
Block a user