Skip to content

Commit

Permalink
feat(action): Add filters to ID selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
zachowj committed Aug 17, 2024
1 parent 9ecb2d6 commit 8d34e43
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 46 deletions.
166 changes: 164 additions & 2 deletions src/editor/components/idSelector/IdSelector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { HassEntity } from 'home-assistant-js-websocket';

import { IdSelectorType } from '../../../common/const';
import { ActionTargetFilter } from '../../../nodes/action/editor/targets';
import {
HassArea,
HassAreas,
HassDevice,
HassFloor,
HassLabel,
} from '../../../types/home-assistant';
import * as haServer from '../../haserver';
import { i18n } from '../../i18n';
import { createRow } from './elements';
import { createSelectOptions } from './virtual-select';
Expand All @@ -9,28 +20,164 @@ interface EditableListButton {
class: string;
click: (evt: any) => void;
}
export interface TargetData {
entities: HassEntity[];
devices: HassDevice[];
areas: HassAreas;
floors: HassFloor[];
labels: HassLabel[];
}

export default class IdSelector {
#$element: JQuery<HTMLElement>;
#headerText: string;
#types: IdSelectorType[];
#filter: ActionTargetFilter[];
#targetData: TargetData;

constructor({
types,
element,
headerText,
filter,
}: {
types: IdSelectorType[];
element: string;
headerText: string;
filter?: ActionTargetFilter[];
}) {
this.#types = types;
this.#$element = $(element);
this.#headerText = headerText;
this.#filter = filter ?? [];
this.#targetData = this.#createTargetData();

this.init();
}

#createTargetData(): TargetData {
const entityRegistry = haServer.getEntityRegistry();
const entities = haServer.getEntities();
const devices = haServer.getDevices();
const areas = haServer.getAreas();
const floors = haServer.getFloors();
const labels = haServer.getLabels();

if (this.#filter.length === 0) {
return {
entities,
devices,
areas,
floors,
labels,
};
}

const filteredEntities: HassEntity[] = [];
const filteredDevices: HassDevice[] = [];
const filteredAreas: HassAreas = [];
const filteredFloors: HassFloor[] = [];
const filteredLabels: HassLabel[] = [];

for (const filter of this.#filter) {
const {
integration,
domain,
device_class: deviceClass,
supported_features: supportedFeatures,
} = filter;

for (const entity of entityRegistry) {
// Skip disabled entities
if (entity.disabled_by !== null) {
continue;
}

// Skip entities that are not part of the integration
if (integration && entity.platform !== integration) {
continue;
}

// Skip entities that are not part of the domain
const entityDomain = entity.entity_id.split('.')[0];
if (domain?.length && !domain.includes(entityDomain)) {
continue;
}

const state = haServer.getEntity(entity.entity_id);

// Skip entities that are not part of the device class
if (
deviceClass?.length &&
state.attributes.device_class !== undefined &&
!deviceClass.includes(state.attributes.device_class)
) {
continue;
}

// Skip entities that do not have the supported features
if (
supportedFeatures !== undefined &&
state.attributes.supported_features !== undefined &&
(state.attributes.supported_features &
supportedFeatures) ===
0
) {
continue;
}

// Add devices that the entity is part of
let device: HassDevice | undefined;
if (entity.device_id) {
device = devices.find((d) => d.id === entity.device_id);
if (device) {
pushIfNotExist(filteredDevices, device);
}
}

// Add areas that the entity is part of
let area: HassArea | undefined;
const areaId = entity.area_id ?? device?.area_id;
if (areaId) {
area = areas.find((a) => a.area_id === areaId);
if (area) {
pushIfNotExist(filteredAreas, area);
}
}

// Add floors that the entity is part of
if (area?.floor_id) {
const floor = floors.find(
(f) => f.floor_id === area?.floor_id,
);
if (floor) {
pushIfNotExist(filteredFloors, floor);
}
}

// Add labels that the entity is part of
if (entity.labels.length) {
for (const label of entity.labels) {
const l = labels.find((l) => l.label_id === label);
if (l) {
filteredLabels.push(l);
}
}
}

filteredEntities.push(state);
}
}

return {
entities: filteredEntities,
devices: filteredDevices,
areas: filteredAreas,
floors: filteredFloors,
labels: filteredLabels,
};
}

#createAddButton(type: IdSelectorType) {
const button: EditableListButton = {
label: i18n(
Expand All @@ -55,7 +202,6 @@ export default class IdSelector {
}

init() {
// padding: 4px 0px 8px;
this.#$element.addClass('id-selector');
this.#$element.editableList({
addButton: false,
Expand All @@ -67,6 +213,7 @@ export default class IdSelector {
$container,
data.type,
data.value,
this.#targetData,
);
$elements.forEach((element) => $container.append(element));
},
Expand All @@ -84,6 +231,8 @@ export default class IdSelector {
}

refreshOptions() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _self = this;
this.#$element.editableList('items').each(function () {
const $li = $(this);
const { type } = $li.data('data');
Expand All @@ -94,12 +243,18 @@ export default class IdSelector {

const $vs = $li.find('.virtual-select');
if ($vs.length) {
const options = createSelectOptions(type);
const options = createSelectOptions(_self.#targetData, type);
// @ts-expect-error - setOptions is not recognized
$vs[0].setOptions(options, true);
}
});
}

updateFilter(filter: ActionTargetFilter[]) {
this.#filter = filter;
this.#targetData = this.#createTargetData();
this.refreshOptions();
}
}

function isIdSelectorType(value: string): value is IdSelectorType {
Expand Down Expand Up @@ -175,3 +330,10 @@ export function getSelectedIds(elementId: string): SelectedIds {
[IdSelectorType.Regex]: Array.from(selectedIds[IdSelectorType.Regex]),
};
}

// Push an element to an array if it does not exist
function pushIfNotExist<T>(array: T[], element: T) {
if (!array.includes(element)) {
array.push(element);
}
}
4 changes: 3 additions & 1 deletion src/editor/components/idSelector/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IdSelectorType } from '../../../common/const';
import { openEntityFilter } from '../../editors/entity-filter';
import { getEntities } from '../../haserver';
import { i18n } from '../../i18n';
import { TargetData } from './IdSelector';
import { createVirtualSelect } from './virtual-select';

declare const RED: EditorRED;
Expand Down Expand Up @@ -126,6 +127,7 @@ export function createRow(
$container: JQuery<HTMLElement>,
type: IdSelectorType,
value: string,
targetData: TargetData,
): JQuery<HTMLElement>[] {
const $row = $('<div>', {
class: 'id-selector-row',
Expand All @@ -141,7 +143,7 @@ export function createRow(
case IdSelectorType.Device:
case IdSelectorType.Entity:
case IdSelectorType.Label:
createVirtualSelect(type, value).appendTo($wrapper);
createVirtualSelect(targetData, type, value).appendTo($wrapper);
createCopyButton($container).appendTo($wrapper);
break;
case IdSelectorType.Substring:
Expand Down
21 changes: 13 additions & 8 deletions src/editor/components/idSelector/virtual-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { SelectorType } from '../../../nodes/config-server/editor';
import { VirtualSelectOption } from '../../../types/virtual-select';
import { getUiSettings } from '../../haserver';
import * as haServer from '../../haserver';
import { TargetData } from './IdSelector';

export function createSelectOptions(
data: TargetData,
type: IdSelectorType,
): VirtualSelectOption[] {
const list: VirtualSelectOption[] = [];
Expand All @@ -13,7 +15,7 @@ export function createSelectOptions(

switch (type) {
case IdSelectorType.Floor:
haServer.getFloors().forEach((floor) => {
data.floors.forEach((floor) => {
list.push({
label: floor.name,
value: floor.floor_id,
Expand All @@ -22,7 +24,7 @@ export function createSelectOptions(
});
break;
case IdSelectorType.Area:
haServer.getAreas().forEach((area) => {
data.areas.forEach((area) => {
list.push({
label: area.name,
value: area.area_id,
Expand All @@ -31,7 +33,7 @@ export function createSelectOptions(
});
break;
case IdSelectorType.Device:
haServer.getDevices().forEach((device) => {
data.devices.forEach((device) => {
const label = useDeviceId
? device.id
: device.name_by_user || device.name;
Expand All @@ -44,8 +46,7 @@ export function createSelectOptions(
});
break;
case IdSelectorType.Entity: {
const entities = haServer.getEntities();
entities.forEach((entity) => {
data.entities.forEach((entity) => {
const label = useEntityId
? entity.entity_id
: entity.attributes.friendly_name || entity.entity_id;
Expand All @@ -61,7 +62,7 @@ export function createSelectOptions(
break;
}
case IdSelectorType.Label:
haServer.getLabels().forEach((label) => {
data.labels.forEach((label) => {
list.push({
label: label.name,
value: label.label_id,
Expand Down Expand Up @@ -93,7 +94,11 @@ function createVirtualSelectOptions(type: IdSelectorType): Record<string, any> {
}
}

export function createVirtualSelect(type: IdSelectorType, value: string): any {
export function createVirtualSelect(
data: TargetData,
type: IdSelectorType,
value: string,
): any {
const $div = $('<div>', {
class: 'virtual-select',
style: 'flex:1;',
Expand All @@ -105,7 +110,7 @@ export function createVirtualSelect(type: IdSelectorType, value: string): any {
hideClearButton: true,
search: true,
maxWidth: '100%',
options: createSelectOptions(type),
options: createSelectOptions(data, type),
placeholder: '',
allowNewOption: true,
optionsCount: 6,
Expand Down
4 changes: 4 additions & 0 deletions src/editor/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ export function getEntityFromRegistry(
return entityRegistry[serverId].find((entry) => entry.id === registryId);
}

export function getEntityRegistry(serverId: string): HassEntityRegistryEntry[] {
return entityRegistry[serverId] ?? [];
}

export function getEntities(serverId: string): HassEntities {
return entities[serverId] ?? {};
}
Expand Down
8 changes: 8 additions & 0 deletions src/editor/haserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export const getDevices = (): HassDevices => {
return haData.getDevices(serverId);
};

export const getEntity = (entityId: string): HassEntity => {
return haData.getEntity(serverId, entityId);
};

export const getEntityRegistry = () => {
return haData.getEntityRegistry(serverId);
};

export const getEntities = (): HassEntity[] => {
return Object.values(haData.getEntities(serverId));
};
Expand Down
4 changes: 3 additions & 1 deletion src/nodes/action/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { i18n } from '../../../editor/i18n';
import { insertSocialBar } from '../../../editor/socialbar';
import { OutputProperty } from '../../../editor/types';
import { loadExampleData, updateServiceSelection } from './service-table';
import { getValidTargets, ValidTarget } from './targets';
import { getTargetFilters, getValidTargets, ValidTarget } from './targets';
import { buildDomainServices } from './utils';

declare const RED: EditorRED;
Expand Down Expand Up @@ -136,6 +136,7 @@ const ActionEditor: EditorNodeDef<ActionEditorNodeProperties> = {
IdSelectorType.Label,
],
headerText: i18n('ha-action.label.targets'),
filter: getTargetFilters(),
});
const ids = {
[IdSelectorType.Floor]: this.floorId,
Expand All @@ -154,6 +155,7 @@ const ActionEditor: EditorNodeDef<ActionEditorNodeProperties> = {
$haAction
.on('change', () => {
updateServiceSelection();
idSelector.updateFilter(getTargetFilters());
const action = $haAction.val() as string;
const showTargets = getValidTargets(action) === ValidTarget.All;
const $formRow = $('#target-list').parents('.form-row');
Expand Down
Loading

0 comments on commit 8d34e43

Please sign in to comment.