Skip to content

Commit

Permalink
fix(device): Fix device node losing trigger id
Browse files Browse the repository at this point in the history
fix(device): Fix translations not loading
Home Assistant changed the entity_id field from being an entity id to the id of the entity registry

fix(device)!: Fix device action to send entity to HA

fixes #1032, fixes #634, fixes #1045
  • Loading branch information
zachowj committed Aug 10, 2024
1 parent 338aada commit 48cfa54
Show file tree
Hide file tree
Showing 18 changed files with 419 additions and 402 deletions.
2 changes: 2 additions & 0 deletions src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
updateDevices,
updateEntities,
updateEntity,
updateEntityRegistry,
updateFloors,
updateLabels,
updateServices,
Expand Down Expand Up @@ -65,6 +66,7 @@ RED.comms.subscribe('homeassistant/labels/#', updateLabels);
RED.comms.subscribe('homeassistant/integration/#', updateIntegration);
RED.comms.subscribe('homeassistant/services/#', updateServices);
RED.comms.subscribe('homeassistant/targetDomains/#', updateTargetDomains);
RED.comms.subscribe('homeassistant/entityRegistry/#', updateEntityRegistry);
RED.comms.subscribe(PRINT_TO_DEBUG_TOPIC, printToDebugPanel);
setupMigrations();
setupEditors();
Expand Down
17 changes: 17 additions & 0 deletions src/editor/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import {
HassArea,
HassDevice,
HassEntityRegistryEntry,
HassFloor,
HassLabel,
} from '../types/home-assistant';
Expand All @@ -21,6 +22,7 @@ const floors: { [serverId: string]: HassFloor[] } = {};
const labels: { [serverId: string]: HassLabel[] } = {};
const services: { [serverId: string]: HassServices } = {};
const targetDomains: { [serverId: string]: HassTargetDomains } = {};
const entityRegistry: { [serverId: string]: HassEntityRegistryEntry[] } = {};

export function updateAreas(topic: string, data: HassArea[]): void {
const serverId = parseServerId(topic);
Expand All @@ -38,6 +40,14 @@ export function updateEntity(topic: string, data: HassEntity): void {
entities[serverId][data.entity_id] = data;
}

export function updateEntityRegistry(
topic: string,
data: HassEntityRegistryEntry[],
): void {
const serverId = parseServerId(topic);
entityRegistry[serverId] = data;
}

export function updateEntities(topic: string, data: HassEntities): void {
const serverId = parseServerId(topic);
entities[serverId] = data;
Expand Down Expand Up @@ -200,6 +210,13 @@ export function getEntity(serverId: string, entityId: string): HassEntity {
return entities[serverId][entityId];
}

export function getEntityFromRegistry(
serverId: string,
registryId: string,
): HassEntityRegistryEntry | undefined {
return entityRegistry[serverId].find((entry) => entry.id === registryId);
}

export function getEntities(serverId: string): HassEntities {
return entities[serverId] ?? {};
}
Expand Down
2 changes: 1 addition & 1 deletion src/editor/exposenode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function getValues() {
const NodeMinIntegraionVersion = {
[NodeType.BinarySensor]: '1.1.0',
[NodeType.Button]: '1.0.4',
[NodeType.Device]: '3.0.0',
[NodeType.Device]: '4.0.2',
[NodeType.Number]: '1.3.0',
[NodeType.Select]: '1.4.0',
[NodeType.Sentence]: '2.2.0',
Expand Down
65 changes: 21 additions & 44 deletions src/homeAssistant/Websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import {
subscribeAreaRegistry,
subscribeDeviceRegistry,
subscribeEntityRegistry,
subscribeEntityRegistryDisplay,
subscribeFloorRegistry,
subscribeLabelRegistry,
} from './collections';
Expand Down Expand Up @@ -240,46 +239,6 @@ export default class Websocket {
);
}

// the config/entity_registry/list_for_display endpoint was added in HA version 2023.3.0
// fallback to the config/entity_registry/list endpoint for older versions
// config/entity_registry/list outputs a larger payload, so we only want to use it if we have to
#subscribeEntityRegistry() {
if (atLeastHaVersion(this.client.haVersion, 2023, 3)) {
subscribeEntityRegistryDisplay(this.client, (entityReg) => {
const entities = entityReg.entities.map((entity) => {
return {
entity_id: entity.ei,
device_id: entity.di,
area_id: entity.ai,
platform: entity.pl,
entity_category:
entity.ec !== undefined
? entityReg.entity_categories[entity.ec]
: undefined,
name: entity.en,
config_entry_id: undefined,
disabled_by: undefined,
icon: undefined,
};
});

this.entities = entities;
this.#emitEvent(HaEvent.RegistryUpdated, {
devices: this.devices,
entities: this.entities,
});
});
} else {
subscribeEntityRegistry(this.client, (entities) => {
this.entities = entities;
this.#emitEvent(HaEvent.RegistryUpdated, {
devices: this.devices,
entities: this.entities,
});
});
}
}

async #haEvents() {
// Home Assistant Events
await this.client.subscribeEvents<HassEvent>(
Expand All @@ -304,7 +263,13 @@ export default class Websocket {
entities: this.entities,
});
});
this.#subscribeEntityRegistry();
subscribeEntityRegistry(this.client, (entities) => {
this.entities = entities;
this.#emitEvent(HaEvent.RegistryUpdated, {
devices: this.devices,
entities: this.entities,
});
});
subscribeFloorRegistry(this.client, (floors) => {
this.#emitEvent(HaEvent.FloorRegistryUpdated, floors);
this.floors = floors;
Expand Down Expand Up @@ -616,10 +581,14 @@ export default class Websocket {
async getDeviceActions(deviceId?: string): Promise<HassDeviceActions> {
if (!this.isConnected || !deviceId) return [];

return this.send<HassDeviceActions>({
const results = await this.send<HassDeviceActions>({
type: 'device_automation/action/list',
device_id: deviceId,
}).catch((e) => {
throw e;
});

return results;
}

async getDeviceActionCapabilities(action: {
Expand All @@ -630,6 +599,8 @@ export default class Websocket {
const results = await this.send<HassDeviceCapabilitiesResponse>({
type: 'device_automation/action/capabilities',
action,
}).catch((e) => {
throw e;
});

return results.extra_fields;
Expand All @@ -638,10 +609,14 @@ export default class Websocket {
async getDeviceTriggers(deviceId?: string): Promise<HassDeviceTriggers> {
if (!this.isConnected || !deviceId) return [];

return this.send<HassDeviceTriggers>({
const results = await this.send<HassDeviceTriggers>({
type: 'device_automation/trigger/list',
device_id: deviceId,
}).catch((e) => {
throw e;
});

return results;
}

async getDeviceTriggerCapabilities(trigger: {
Expand All @@ -652,6 +627,8 @@ export default class Websocket {
const results = await this.send<HassDeviceCapabilitiesResponse>({
type: 'device_automation/trigger/capabilities',
trigger,
}).catch((e) => {
throw e;
});

return results.extra_fields;
Expand Down
34 changes: 0 additions & 34 deletions src/homeAssistant/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { throttle } from 'lodash';
import {
HassAreas,
HassDevices,
HassEntityRegistryDisplayEntryResponse,
HassEntityRegistryEntry,
HassFloor,
HassLabel,
Expand Down Expand Up @@ -110,39 +109,6 @@ export function subscribeEntityRegistry(
collection.subscribe(cb);
}

export function subscribeEntityRegistryDisplay(
conn: Connection,
cb: (state: HassEntityRegistryDisplayEntryResponse) => void,
): void {
const fetchEntityRegistry = (conn: Connection) =>
conn.sendMessagePromise<HassEntityRegistryDisplayEntryResponse>({
type: 'config/entity_registry/list_for_display',
});

const subscribeUpdates = (
conn: Connection,
store: Store<HassEntityRegistryDisplayEntryResponse>,
) =>
conn.subscribeEvents(
throttle(
() =>
fetchEntityRegistry(conn).then((devices) =>
store.setState(devices, true),
),
500,
),
'entity_registry_updated',
);

const collection = getCollection(
conn,
'_entityRegistryDisplay',
fetchEntityRegistry,
subscribeUpdates,
);
collection.subscribe(cb);
}

export function subscribeFloorRegistry(
conn: Connection,
cb: (state: HassFloor[]) => void,
Expand Down
47 changes: 1 addition & 46 deletions src/nodes/config-server/Comms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ import {
HassStateChangedEvent,
} from '../../types/home-assistant';

const convertSetToArray = (obj: { [key: string]: Set<string> }) => {
const result: { [key: string]: string[] } = {};
Object.keys(obj).forEach((key) => {
result[key] = Array.from(obj[key]);
});
return result;
};

export default class Comms {
readonly #clientEvents: ClientEvents;
readonly #homeAssistant: HomeAssistant;
Expand Down Expand Up @@ -76,48 +68,11 @@ export default class Comms {
}

onRegistryUpdate({
devices,
entities,
}: {
areas: HassAreas;
devices: HassDevices;
entities: HassEntityRegistryEntry[];
}): void {
const areaDomains: { [key: string]: Set<string> } = {};
const deviceDomains: { [key: string]: Set<string> } = {};
entities.forEach((entity) => {
if (entity.area_id) {
if (!(entity.area_id in areaDomains)) {
areaDomains[entity.area_id] = new Set<string>();
}
areaDomains[entity.area_id].add(entity.entity_id.split('.')[0]);
}
if (entity.device_id) {
if (!(entity.device_id in deviceDomains)) {
deviceDomains[entity.device_id] = new Set<string>();
}
deviceDomains[entity.device_id].add(
entity.entity_id.split('.')[0],
);
}
});

devices.forEach((device) => {
if (device.area_id) {
if (!(device.area_id in areaDomains)) {
areaDomains[device.area_id] = new Set<string>();
}
areaDomains[device.area_id] = new Set([
...areaDomains[device.area_id],
...(deviceDomains[device.id] ?? []),
]);
}
});

this.publish('targetDomains', {
areas: convertSetToArray(areaDomains),
devices: convertSetToArray(deviceDomains),
});
this.publish('entityRegistry', entities);
}

onIntegrationEvent(eventType: string): void {
Expand Down
4 changes: 4 additions & 0 deletions src/nodes/device/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum DeviceType {
Action = 'action',
Trigger = 'trigger',
}
8 changes: 6 additions & 2 deletions src/nodes/device/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@

<div class="form-row">
<label for="node-input-device" data-i18n="ha-device.label.device"></label>
<select id="node-input-device" style="width: 70%"></select>
<div id="ha-device"></div>
</div>

<div class="form-row">
<label for="event" data-i18n="ha-device.label.trigger"></label>
<label
for="event"
data-i18n="ha-device.label.trigger"
style="text-transform: capitalize"
></label>
<select id="event" style="width: 70%" disabled></select>
</div>

Expand Down
4 changes: 4 additions & 0 deletions src/nodes/device/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@
}
}
}

#ha-device {
margin-left: 4px;
}
32 changes: 19 additions & 13 deletions src/nodes/device/editor/action.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import * as haServer from '../../../editor/haserver';
import {
HassDeviceActions,
HassDeviceCapabilities,
} from '../../../types/home-assistant';
import { DeviceEndpoint, fetchDeviceData } from './utils';

export const inputCount = 1;

export const getCapabilitiesList = async (
export async function getCapabilitiesList(
action: any,
): Promise<HassDeviceCapabilities> =>
await haServer.fetch('deviceActionCapabilities', {
action,
});
): Promise<HassDeviceCapabilities> {
return fetchDeviceData<HassDeviceCapabilities>(
DeviceEndpoint.DeviceActionCapabilities,
{ event: JSON.stringify(action) },
'ha-device.error.failed_to_fetch_capabilities',
);
}

export const getEventList = async (
export async function getEventList(
deviceId: string,
): Promise<HassDeviceActions> =>
await haServer.fetch('deviceActions', {
deviceId,
});
): Promise<HassDeviceActions> {
return fetchDeviceData<HassDeviceActions>(
DeviceEndpoint.DeviceActions,
{ deviceId },
'ha-device.error.failed_to_fetch_actions',
);
}

export const setDefaultOutputs = () => {
export function setDefaultOutputs() {
return [];
};
}
Loading

0 comments on commit 48cfa54

Please sign in to comment.