diff --git a/src/common/controllers/EposeAsController.ts b/src/common/controllers/EposeAsController.ts new file mode 100644 index 0000000000..f09cd69157 --- /dev/null +++ b/src/common/controllers/EposeAsController.ts @@ -0,0 +1,28 @@ +import { EntityConfigNode } from '../../nodes/entity-config'; +import { BaseNode } from '../../types/nodes'; +import { TriggerPayload } from '../integration/BidirectionalEntityIntegration'; +import OutputController, { + OutputControllerConstructor, +} from './OutputController'; + +export interface ExposeAsControllerConstructor + extends OutputControllerConstructor { + exposeAsConfigNode?: EntityConfigNode; +} + +export default abstract class ExposeAsController< + T extends BaseNode = BaseNode +> extends OutputController { + protected readonly exposeAsConfigNode?: EntityConfigNode; + + constructor(props: ExposeAsControllerConstructor) { + super(props); + this.exposeAsConfigNode = props.exposeAsConfigNode; + } + + get isEnabled(): boolean { + return this.exposeAsConfigNode?.state?.isEnabled() ?? false; + } + + public abstract onTriggered(data: TriggerPayload): void; +} diff --git a/src/editor/convert-entity.ts b/src/editor/convert-entity.ts index af682f5072..cf7322be8e 100644 --- a/src/editor/convert-entity.ts +++ b/src/editor/convert-entity.ts @@ -1,4 +1,4 @@ -// TODO: Can be removed after ha-entity is removed +// TODO: Remove for version 1.0 import { EditorRED } from 'node-red'; import { EntityType, NodeType } from '../const'; @@ -183,3 +183,48 @@ export const convertEntityNode = (node: EntityProperties) => { } RED.view.redraw(true); }; + +export const convertEventNode = (node: any) => { + // Save the wires so we can add them to the new node + const wires = { + // @ts-expect-error - function is not defined in types + source: RED.nodes.getNodeLinks(node.id), + // @ts-expect-error - function is not defined in types + target: RED.nodes.getNodeLinks(node.id, 1), + }; + + const newId = generateId(); + // If the node is in a group remove it so NR doesn't think the new config node is in the group + if (node.g) { + const oldEntityNode = RED.nodes.node(node.id); + if (oldEntityNode) { + RED.group.removeFromGroup(RED.nodes.group(node.g), oldEntityNode); + } + } + RED.nodes.remove(node.id); + RED.nodes.import({ + type: NodeType.EntityConfig, + id: node.id, + server: node.server, + deviceConfig: '', + name: `exposed as for ${node.name || node.id}`, + version: RED.settings.get('haEntityConfigVersion', 0), + entityType: EntityType.Switch, + haConfig: node.haConfig ?? [], + resend: false, + }); + RED.nodes.import({ ...node, id: newId, exposeAsEntityConfig: node.id }); + addLinks(newId, wires); + const entityNode = RED.nodes.node(newId); + if (entityNode) { + RED.nodes.moveNodeToTab(entityNode, node.z); + if (node.g) { + RED.group.addToGroup(RED.nodes.group(node.g), entityNode); + } + // @ts-expect-error - changed defined as readonly + entityNode.changed = true; + } + RED.view.redraw(true); + + return newId; +}; diff --git a/src/editor/types.ts b/src/editor/types.ts index aecf3c9003..c78dae02f0 100644 --- a/src/editor/types.ts +++ b/src/editor/types.ts @@ -40,9 +40,11 @@ export interface HassNodeProperties debugenabled?: boolean; server?: string; entityConfigNode?: string; - exposeToHomeAssistant?: boolean; outputs?: number | undefined; haConfig?: HassExposedConfig[]; + + // TODO: remove after controllers are converted to TypeScript + exposeToHomeAssistant?: boolean; } export interface HassTargetDomains { diff --git a/src/editor/version.ts b/src/editor/version.ts index 19328dbb72..a6b646fb4f 100644 --- a/src/editor/version.ts +++ b/src/editor/version.ts @@ -2,7 +2,11 @@ import { EditorNodeInstance, EditorRED } from 'node-red'; import { NodeType } from '../const'; import { migrate } from '../helpers/migrate'; -import { convertEntityNode, EntityProperties } from './convert-entity'; +import { + convertEntityNode, + convertEventNode, + EntityProperties, +} from './convert-entity'; import { i18n } from './i18n'; import { HassNodeProperties } from './types'; @@ -29,6 +33,8 @@ export function versionCheckOnEditPrepare( ) { if (!isHomeAssistantNode(node) || isCurrentVersion(node)) return; + node = migrateNode(node); + // the close event will not fire if the editor was already opened if (!isHomeAssistantConfigNode(node)) { RED.events.on('editor:close', function reopen() { @@ -36,18 +42,30 @@ export function versionCheckOnEditPrepare( RED.editor.edit(node); }); } - migrateNode(node); + RED.nodes.dirty(true); RED.tray.close(); RED.notify(i18n('home-assistant.ui.migrations.node_schema_updated')); } +const exposedEventNodes: NodeType[] = []; + function migrateNode(node: EditorNodeInstance) { - const data = RED.nodes.convertNode(node, false); + let data = RED.nodes.convertNode(node, false); + + // TODO: Remove for version 1.0 + if ( + exposedEventNodes.includes(node.type as unknown as NodeType) && + node.exposeToHomeAssistant === true + ) { + const newId = convertEventNode(data as unknown as EntityProperties); + node = RED.nodes.node(newId) as EditorNodeInstance; + data = RED.nodes.convertNode(node, false); + } const migratedData: HassNodeProperties = migrate(data); - // TODO: Can be removed after ha-entity is removed + // TODO: Remove for version 1.0 if (migratedData.type === NodeType.Entity) { convertEntityNode(migratedData as unknown as EntityProperties); } @@ -70,6 +88,8 @@ function migrateNode(node: EditorNodeInstance) { if ($upgradeHaNode.is(':visible') && getOldNodeCount() === 0) { $upgradeHaNode.hide(); } + + return node; } function migrateAllNodes() { diff --git a/src/helpers/node.ts b/src/helpers/node.ts index 5838ad3732..208bc6dcb8 100644 --- a/src/helpers/node.ts +++ b/src/helpers/node.ts @@ -71,6 +71,12 @@ export function getConfigNodes(node: EntityNode) { }; } +export function getExposeAsConfigNode( + nodeId?: string +): EntityConfigNode | undefined { + return getNode(nodeId) as EntityConfigNode; +} + function isConfigNode(node: BaseNode | EntityNode): boolean { return ( node.type === NodeType.DeviceConfig || diff --git a/src/nodes/entity-config/editor/helpers.ts b/src/nodes/entity-config/editor/helpers.ts index 84af7bc4ef..86812803fb 100644 --- a/src/nodes/entity-config/editor/helpers.ts +++ b/src/nodes/entity-config/editor/helpers.ts @@ -191,9 +191,14 @@ export const createRow = ( return $row; }; -export const saveEntityType = (type: EntityType) => { - $('#node-input-lookup-entityConfig').on('click', function () { - if ($('#node-input-entityConfig').val() === '_ADD_') { +type EntityTypeSelector = 'entityConfig' | 'exposeAsEntityConfig'; + +export const saveEntityType = ( + type: EntityType, + selector: EntityTypeSelector = 'entityConfig' +) => { + $(`#node-input-lookup-${selector}`).on('click', function () { + if ($(`#node-input-${selector}`).val() === '_ADD_') { $('body').data('haEntityType', type); } }); diff --git a/src/types/home-assistant.ts b/src/types/home-assistant.ts index b5d7ac3306..e1b1f6620b 100644 --- a/src/types/home-assistant.ts +++ b/src/types/home-assistant.ts @@ -88,6 +88,13 @@ export type HassEntity = Omit & { timeSinceChangedMs: number; }; +export type HassEvent = HassEventBase & { + event_type: string; + event: { + [key: string]: any; + }; +}; + export type HassStateChangedEvent = HassEventBase & { event_type: string; entity_id: string; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index c65d5a0f68..7b6e35ded0 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -68,8 +68,9 @@ export interface BaseNodeConfig extends NodeProperties { debugenabled?: boolean; server?: string; entityConfigNode?: string; - exposeToHomeAssistant?: boolean; outputs?: number; + // TODO: Can be removed when controllers are converted to TypeScript + exposeToHomeAssistant?: boolean; } export interface BaseNode extends Node {