Add floor selector (#20295)
This commit is contained in:
parent
1ce3347c2e
commit
85f2016371
|
@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|||
export const mockAreaRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: AreaRegistryEntry[] = []
|
||||
) => hass.mockWS("config/area_registry/list", () => data);
|
||||
) => {
|
||||
hass.mockWS("config/area_registry/list", () => data);
|
||||
const areas = {};
|
||||
data.forEach((area) => {
|
||||
areas[area.area_id] = area;
|
||||
});
|
||||
hass.updateHass({ areas });
|
||||
};
|
||||
|
|
|
@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|||
export const mockDeviceRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DeviceRegistryEntry[] = []
|
||||
) => hass.mockWS("config/device_registry/list", () => data);
|
||||
) => {
|
||||
hass.mockWS("config/device_registry/list", () => data);
|
||||
const devices = {};
|
||||
data.forEach((device) => {
|
||||
devices[device.id] = device;
|
||||
});
|
||||
hass.updateHass({ devices });
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { FloorRegistryEntry } from "../../../src/data/floor_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockFloorRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: FloorRegistryEntry[] = []
|
||||
) => hass.mockWS("config/floor_registry/list", () => data);
|
|
@ -0,0 +1,7 @@
|
|||
import { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockLabelRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: LabelRegistryEntry[] = []
|
||||
) => hass.mockWS("config/label_registry/list", () => data);
|
|
@ -17,6 +17,10 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
|
|||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
|
@ -100,7 +104,7 @@ const DEVICES = [
|
|||
const AREAS: AreaRegistryEntry[] = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
floor_id: null,
|
||||
floor_id: "ground",
|
||||
name: "Backyard",
|
||||
icon: null,
|
||||
picture: null,
|
||||
|
@ -109,7 +113,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
floor_id: null,
|
||||
floor_id: "first",
|
||||
name: "Bedroom",
|
||||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
|
@ -118,7 +122,7 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
floor_id: null,
|
||||
floor_id: "ground",
|
||||
name: "Livingroom",
|
||||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
|
@ -127,6 +131,45 @@ const AREAS: AreaRegistryEntry[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const FLOORS: FloorRegistryEntry[] = [
|
||||
{
|
||||
floor_id: "ground",
|
||||
name: "Ground floor",
|
||||
level: 0,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
},
|
||||
{
|
||||
floor_id: "first",
|
||||
name: "First floor",
|
||||
level: 1,
|
||||
icon: "mdi:numeric-1",
|
||||
aliases: [],
|
||||
},
|
||||
{
|
||||
floor_id: "second",
|
||||
name: "Second floor",
|
||||
level: 2,
|
||||
icon: "mdi:numeric-2",
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
|
||||
const LABELS: LabelRegistryEntry[] = [
|
||||
{
|
||||
label_id: "energy",
|
||||
name: "Energy",
|
||||
icon: null,
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
label_id: "entertainment",
|
||||
name: "Entertainment",
|
||||
icon: "mdi:popcorn",
|
||||
color: "blue",
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
|
||||
|
@ -134,7 +177,12 @@ const SCHEMAS: {
|
|||
{
|
||||
name: "One of each",
|
||||
input: {
|
||||
label: { name: "Label", selector: { label: {} } },
|
||||
floor: { name: "Floor", selector: { floor: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
device: { name: "Device", selector: { device: {} } },
|
||||
entity: { name: "Entity", selector: { entity: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
state: {
|
||||
name: "State",
|
||||
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
|
||||
|
@ -143,15 +191,12 @@ const SCHEMAS: {
|
|||
name: "Attribute",
|
||||
selector: { attribute: { entity_id: "" } },
|
||||
},
|
||||
device: { name: "Device", selector: { device: {} } },
|
||||
config_entry: {
|
||||
name: "Integration",
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
|
@ -300,6 +345,8 @@ const SCHEMAS: {
|
|||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||
floor: { name: "Floor", selector: { floor: { multiple: true } } },
|
||||
label: { name: "Label", selector: { label: { multiple: true } } },
|
||||
select: {
|
||||
name: "Select Multiple",
|
||||
selector: {
|
||||
|
@ -356,6 +403,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
|||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockFloorRegistry(hass, FLOORS);
|
||||
mockLabelRegistry(hass, LABELS);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
|
|
|
@ -274,7 +274,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||
if (areaIds) {
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
outputFloors = outputFloors.filter((floor) =>
|
||||
floorAreaLookup[floor.floor_id].some((area) =>
|
||||
floorAreaLookup[floor.floor_id]?.some((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-floor-picker";
|
||||
|
||||
@customElement("ha-floors-picker")
|
||||
export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd = false;
|
||||
|
||||
/**
|
||||
* Show only floors with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no floors with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only floors with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ attribute: "picked-floor-label" })
|
||||
public pickedFloorLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-floor-label" })
|
||||
public pickFloorLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentFloors = this._currentFloors;
|
||||
return html`
|
||||
${currentFloors.map(
|
||||
(floor) => html`
|
||||
<div>
|
||||
<ha-floor-picker
|
||||
.curValue=${floor}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${floor}
|
||||
.label=${this.pickedFloorLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._floorChanged}
|
||||
></ha-floor-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-floor-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickFloorLabel}
|
||||
.helper=${this.helper}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
.required=${this.required && !currentFloors.length}
|
||||
@value-changed=${this._addFloor}
|
||||
.excludeFloors=${currentFloors}
|
||||
></ha-floor-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentFloors(): string[] {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateFloors(floors) {
|
||||
this.value = floors;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: floors,
|
||||
});
|
||||
}
|
||||
|
||||
private _floorChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const curValue = (ev.currentTarget as any).curValue;
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue === curValue) {
|
||||
return;
|
||||
}
|
||||
const currentFloors = this._currentFloors;
|
||||
if (!newValue || currentFloors.includes(newValue)) {
|
||||
this._updateFloors(currentFloors.filter((ent) => ent !== curValue));
|
||||
return;
|
||||
}
|
||||
this._updateFloors(
|
||||
currentFloors.map((ent) => (ent === curValue ? newValue : ent))
|
||||
);
|
||||
}
|
||||
|
||||
private _addFloor(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const toAdd = ev.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as any).value = "";
|
||||
const currentFloors = this._currentFloors;
|
||||
if (currentFloors.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateFloors([...currentFloors, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-floors-picker": HaFloorsPicker;
|
||||
}
|
||||
}
|
|
@ -87,8 +87,12 @@ export class HaAreaSelector extends LitElement {
|
|||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.deviceFilter=${this.selector.area?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.area?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-picker>
|
||||
|
@ -102,8 +106,12 @@ export class HaAreaSelector extends LitElement {
|
|||
.helper=${this.helper}
|
||||
.pickAreaLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.deviceFilter=${this.selector.area?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.area?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-areas-picker>
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { FloorSelector } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-floor-picker";
|
||||
import "../ha-floors-picker";
|
||||
|
||||
@customElement("ha-selector-floor")
|
||||
export class HaFloorSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: FloorSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: FloorSelector) {
|
||||
return (
|
||||
(selector.floor?.entity &&
|
||||
ensureArray(selector.floor.entity).some(
|
||||
(filter) => filter.integration
|
||||
)) ||
|
||||
(selector.floor?.device &&
|
||||
ensureArray(selector.floor.device).some((device) => device.integration))
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
} else if (!this.selector.floor?.multiple && Array.isArray(this.value)) {
|
||||
this.value = this.value[0];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.selector.floor?.multiple) {
|
||||
return html`
|
||||
<ha-floor-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.floor?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.floor?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-floor-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-floors-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.pickFloorLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.floor?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.floor?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-floors-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.floor?.entity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.floor.entity).some((filter) =>
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (!this.selector.floor?.device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return ensureArray(this.selector.floor.device).some((filter) =>
|
||||
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-floor": HaFloorSelector;
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ const LOAD_ELEMENTS = {
|
|||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
floor: () => import("./ha-selector-floor"),
|
||||
label: () => import("./ha-selector-label"),
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
|
|
|
@ -31,6 +31,7 @@ export type Selector =
|
|||
| DateSelector
|
||||
| DateTimeSelector
|
||||
| DeviceSelector
|
||||
| FloorSelector
|
||||
| LegacyDeviceSelector
|
||||
| DurationSelector
|
||||
| EntitySelector
|
||||
|
@ -170,6 +171,14 @@ export interface DeviceSelector {
|
|||
} | null;
|
||||
}
|
||||
|
||||
export interface FloorSelector {
|
||||
floor: {
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LegacyDeviceSelector {
|
||||
device: DeviceSelector["device"] & {
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue