diff --git a/docs/node/webhook.md b/docs/node/webhook.md
index df4d203fbe..4fa916950f 100644
--- a/docs/node/webhook.md
+++ b/docs/node/webhook.md
@@ -9,23 +9,17 @@ in Home Assistant for this node to function_
## Configuration
-### ID
+### ID
- Type: `string`
A string to be used for the webhook URL in Home Assistant.
-### Payload
+### Allowed Methods
-- Type: `string`
-
-Customizable location for the webhook payload. Defaults to msg.payload
-
-### Headers
-
-- Type: `number`
+- Type: `list`
-Customizable location for the webhook request headers.
+A list of allowed methods that Home Assistant will accept for the webhook. At least one method must be selected.
## Outputs
diff --git a/locales/en-US/common.json b/locales/en-US/common.json
index da69c2601c..957d023a03 100644
--- a/locales/en-US/common.json
+++ b/locales/en-US/common.json
@@ -67,6 +67,7 @@
"off": "off",
"on": "on",
"pressed": "pressed",
+ "received": "received",
"registered": "registered",
"running": "running",
"sending": "sending",
diff --git a/locales/en-US/webhook.json b/locales/en-US/webhook.json
new file mode 100644
index 0000000000..d304d6e45e
--- /dev/null
+++ b/locales/en-US/webhook.json
@@ -0,0 +1,11 @@
+{
+ "ha-webhook": {
+ "label": {
+ "allowed_methods": "Allowed Methods",
+ "put": "PUT",
+ "post": "POST",
+ "get": "GET",
+ "head": "HEAD"
+ }
+ }
+}
diff --git a/src/common/integration/Integration.ts b/src/common/integration/Integration.ts
index bd4113a534..e41846d37a 100644
--- a/src/common/integration/Integration.ts
+++ b/src/common/integration/Integration.ts
@@ -22,8 +22,9 @@ export enum MessageType {
Discovery = 'nodered/discovery',
Entity = 'nodered/entity',
RemoveDevice = 'nodered/device/remove',
- UpdateConfig = 'nodered/entity/update_config',
SentenseTrigger = 'nodered/sentence',
+ UpdateConfig = 'nodered/entity/update_config',
+ Webhook = 'nodered/webhook',
}
export interface MessageBase {
diff --git a/src/nodes/webhook/WebhookController.ts b/src/nodes/webhook/WebhookController.ts
new file mode 100644
index 0000000000..b42038674b
--- /dev/null
+++ b/src/nodes/webhook/WebhookController.ts
@@ -0,0 +1,44 @@
+import OutputController, {
+ OutputControllerOptions,
+} from '../../common/controllers/OutputController';
+import { IntegrationEvent } from '../../common/integration/Integration';
+import { NodeMessage } from '../../types/nodes';
+import { WebhookNode } from '.';
+
+interface WebhookResponse {
+ payload: any;
+ headers: Record;
+ params: Record;
+}
+
+type WebhookNodeOptions = OutputControllerOptions;
+
+export default class WebhookController extends OutputController {
+ constructor(props: WebhookNodeOptions) {
+ super(props);
+
+ this.node.addListener(
+ IntegrationEvent.Trigger,
+ this.#onReceivedMessage.bind(this)
+ );
+ }
+
+ #onReceivedMessage(data: WebhookResponse) {
+ const message: NodeMessage = {};
+ try {
+ this.setCustomOutputs(this.node.config.outputProperties, message, {
+ config: this.node.config,
+ data: data.payload,
+ headers: data.headers,
+ params: data.params,
+ });
+ } catch (e) {
+ this.node.error(e);
+ this.status.setFailed('error');
+ return;
+ }
+
+ this.status.setSuccess('home-assistant.status.received');
+ this.node.send(message);
+ }
+}
diff --git a/src/nodes/webhook/WebhookIntegration.ts b/src/nodes/webhook/WebhookIntegration.ts
new file mode 100644
index 0000000000..607d741bef
--- /dev/null
+++ b/src/nodes/webhook/WebhookIntegration.ts
@@ -0,0 +1,42 @@
+import BidirectionalIntegration, {
+ DiscoveryBaseMessage,
+} from '../../common/integration/BidrectionalIntegration';
+import { MessageType } from '../../common/integration/Integration';
+import { WebhookNodeProperties } from '.';
+
+export interface WebhookDiscoveryPayload extends DiscoveryBaseMessage {
+ type: MessageType.Webhook;
+ server_id: string;
+ webhook_id: string;
+ name: string;
+ allowed_methods: string[];
+}
+
+export default class WebhookIntegration extends BidirectionalIntegration {
+ protected getDiscoveryPayload(
+ config: WebhookNodeProperties
+ ): WebhookDiscoveryPayload {
+ const methods = [
+ 'method_post',
+ 'method_get',
+ 'method_put',
+ 'method_head',
+ ] as const;
+
+ const allowedMethods = methods.reduce((acc, method) => {
+ if (config[method]) {
+ acc.push(method.replace('method_', '').toUpperCase());
+ }
+ return acc;
+ }, [] as string[]);
+
+ return {
+ type: MessageType.Webhook,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ server_id: config.server!,
+ webhook_id: config.webhookId,
+ name: config.id,
+ allowed_methods: allowedMethods,
+ };
+ }
+}
diff --git a/src/nodes/webhook/controller.js b/src/nodes/webhook/controller.js
deleted file mode 100644
index 51b4877873..0000000000
--- a/src/nodes/webhook/controller.js
+++ /dev/null
@@ -1,102 +0,0 @@
-const EventsNode = require('../EventsNode');
-const { INTEGRATION_UNLOADED } = require('../../const');
-
-const nodeOptions = {
- config: {
- name: {},
- server: { isNode: true },
- outputs: 1,
- webhookId: {},
- exposeToHomeAssistant: () => true,
- outputProperties: {},
- },
-};
-
-class Webhook extends EventsNode {
- constructor({ node, config, RED, status }) {
- super({ node, config, RED, status, nodeOptions });
-
- if (this.isIntegrationLoaded) {
- this.registerEntity();
- }
- }
-
- onHaEventsClose() {
- super.onHaEventsClose();
-
- this.removeWebhook = null;
- }
-
- onEvent(evt) {
- const message = {};
- try {
- this.setCustomOutputs(this.nodeConfig.outputProperties, message, {
- config: this.nodeConfig,
- data: evt.data.payload,
- headers: evt.data.headers,
- params: evt.data.params,
- });
- } catch (e) {
- this.node.error(e);
- this.status.setFailed('error');
- return;
- }
-
- this.status.setSuccess('Received');
- this.send(message);
- }
-
- onHaIntegration(type) {
- super.onHaIntegration(type);
-
- if (type === INTEGRATION_UNLOADED) {
- if (this.removeWebhook) {
- this.removeWebhook();
- this.removeWebhook = null;
- }
- this.node.error(
- 'Node-RED custom integration has been removed from Home Assistant it is needed for this node to function.'
- );
- this.status.setFailed('Error');
- }
- }
-
- async registerEntity() {
- if (super.registerEntity() === false) {
- return;
- }
-
- if (!this.nodeConfig.webhookId) {
- this.node.error(this.integrationErrorMessage);
- this.status.setFailed('Error');
- return;
- }
-
- if (!this.removeWebhook) {
- this.node.debug(`Adding webhook to HA`);
- this.removeWebhook = await this.homeAssistant.subscribeMessage(
- this.onEvent.bind(this),
- {
- type: 'nodered/webhook',
- webhook_id: this.nodeConfig.webhookId,
- name: this.node.id,
- server_id: this.nodeConfig.server.id,
- },
- { resubscribe: false }
- );
- }
- this.status.setSuccess('Registered');
- this.registered = true;
- }
-
- onClose(removed) {
- super.onClose(removed);
-
- if (this.registered && this.isConnected && this.removeWebhook) {
- this.node.debug('Removing webhook from HA');
- this.removeWebhook().catch(() => {});
- }
- }
-}
-
-module.exports = Webhook;
diff --git a/src/nodes/webhook/editor.html b/src/nodes/webhook/editor.html
index ba8a2e7d97..e74f5187e4 100644
--- a/src/nodes/webhook/editor.html
+++ b/src/nodes/webhook/editor.html
@@ -24,4 +24,36 @@
+
+
diff --git a/src/nodes/webhook/editor.ts b/src/nodes/webhook/editor.ts
index 5b42642af2..11a34a52cb 100644
--- a/src/nodes/webhook/editor.ts
+++ b/src/nodes/webhook/editor.ts
@@ -13,6 +13,10 @@ interface WebhookEditorNodeProperties extends EditorNodeProperties {
server: string;
version: number;
webhookId: string;
+ method_get: boolean;
+ method_head: boolean;
+ method_post: boolean;
+ method_put: boolean;
outputProperties: OutputProperty[];
// deprecated but needed for imports
@@ -32,6 +36,12 @@ function generateId(length: number) {
).join('');
}
+// Check that at least one method is enabled
+function validateMethods(): boolean {
+ const methods = ['method_post', 'method_get', 'method_put', 'method_head'];
+ return methods.some((method) => this[method]);
+}
+
const WebhookEditor: EditorNodeDef = {
category: NodeCategory.HomeAssistant,
color: NodeColor.HaBlue,
@@ -49,6 +59,10 @@ const WebhookEditor: EditorNodeDef = {
version: { value: RED.settings.get('haWebhookVersion', 0) },
outputs: { value: 1 },
webhookId: { value: generateId(32), required: true },
+ method_get: { value: false, validate: validateMethods },
+ method_head: { value: false, validate: validateMethods },
+ method_post: { value: true, validate: validateMethods },
+ method_put: { value: true, validate: validateMethods },
outputProperties: {
value: [
{
@@ -95,6 +109,19 @@ const WebhookEditor: EditorNodeDef = {
$webhookId.val(generateId(32));
});
+ $('[id^=node-input-method_]')
+ .on('change', function () {
+ const isMethodSelected = $('[id^=node-input-method_]').is(
+ ':checked'
+ );
+ if (!isMethodSelected) {
+ $(this).closest('div').addClass('input-error');
+ } else {
+ $(this).closest('div').removeClass('input-error');
+ }
+ })
+ .trigger('change');
+
haOutputs.createOutputs(this.outputProperties, {
extraTypes: ['receivedData', 'headers', 'params', 'triggerId'],
});
diff --git a/src/nodes/webhook/index.ts b/src/nodes/webhook/index.ts
index cca7d1691d..7354925dc6 100644
--- a/src/nodes/webhook/index.ts
+++ b/src/nodes/webhook/index.ts
@@ -1,22 +1,71 @@
-import { NodeDef } from 'node-red';
-
+import { createControllerDependencies } from '../../common/controllers/helpers';
+import ClientEvents from '../../common/events/ClientEvents';
+import Events from '../../common/events/Events';
+import State from '../../common/State';
+import Status from '../../common/status/Status';
import { RED } from '../../globals';
import { migrate } from '../../helpers/migrate';
-import { EventsStatus } from '../../helpers/status';
-import { checkValidServerConfig } from '../../helpers/utils';
-import { BaseNode } from '../../types/nodes';
-import Webhook from './controller';
+import { getServerConfigNode } from '../../helpers/node';
+import { getHomeAssistant } from '../../homeAssistant';
+import {
+ BaseNode,
+ BaseNodeProperties,
+ OutputProperty,
+} from '../../types/nodes';
+import WebhookController from './WebhookController';
+import WebhookIntegration from './WebhookIntegration';
-export default function webhookNode(this: BaseNode, config: NodeDef) {
- RED.nodes.createNode(this, config);
+export interface WebhookNodeProperties extends BaseNodeProperties {
+ webhookId: string;
+ method_get: boolean;
+ method_head: boolean;
+ method_put: boolean;
+ method_post: boolean;
+ local_only: boolean;
+ outputProperties: OutputProperty[];
+}
+
+export interface WebhookNode extends BaseNode {
+ config: WebhookNodeProperties;
+}
+export default function webhookNode(
+ this: WebhookNode,
+ config: WebhookNodeProperties
+) {
+ RED.nodes.createNode(this, config);
this.config = migrate(config);
- checkValidServerConfig(this, this.config.server);
- const status = new EventsStatus(this);
- this.controller = new Webhook({
+
+ const serverConfigNode = getServerConfigNode(this.config.server);
+ const homeAssistant = getHomeAssistant(serverConfigNode);
+ const clientEvents = new ClientEvents({
+ node: this,
+ emitter: homeAssistant.eventBus,
+ });
+ const nodeEvents = new Events({ node: this, emitter: this });
+ const state = new State(this);
+ const status = new Status({
+ config: serverConfigNode.config,
+ nodeEvents,
+ node: this,
+ state,
+ });
+
+ const controllerDeps = createControllerDependencies(this, homeAssistant);
+ const integration = new WebhookIntegration({
+ node: this,
+ clientEvents,
+ homeAssistant,
+ state,
+ });
+ integration.setStatus(status);
+
+ this.controller = new WebhookController({
node: this,
- config: this.config,
- RED,
status,
+ ...controllerDeps,
+ state,
});
+
+ integration.init();
}
diff --git a/src/nodes/webhook/migrations.ts b/src/nodes/webhook/migrations.ts
index 370a494f62..60108eb42f 100644
--- a/src/nodes/webhook/migrations.ts
+++ b/src/nodes/webhook/migrations.ts
@@ -48,4 +48,19 @@ export default [
return newSchema;
},
},
+ {
+ version: 2,
+ up: (schema: any) => {
+ const newSchema = {
+ ...schema,
+ version: 2,
+ method_post: true,
+ method_put: true,
+ method_get: false,
+ method_head: false,
+ };
+
+ return newSchema;
+ },
+ },
];
diff --git a/test/migrations/webhook.test.js b/test/migrations/webhook.test.js
index 3ef9881b7c..07a0eb1373 100644
--- a/test/migrations/webhook.test.js
+++ b/test/migrations/webhook.test.js
@@ -47,6 +47,14 @@ const VERSION_1 = {
headersLocation: undefined,
headersLocationType: undefined,
};
+const VERSION_2 = {
+ ...VERSION_1,
+ version: 2,
+ method_post: true,
+ method_put: true,
+ method_get: false,
+ method_head: false,
+};
describe('Migrations - Webhook Node', function () {
describe('Version 0', function () {
@@ -63,8 +71,15 @@ describe('Migrations - Webhook Node', function () {
expect(migratedSchema).to.eql(VERSION_1);
});
});
+ describe('Version 2', function () {
+ it('should update version 1 to version 2', function () {
+ const migrate = migrations.find((m) => m.version === 2);
+ const migratedSchema = migrate.up(VERSION_1);
+ expect(migratedSchema).to.eql(VERSION_2);
+ });
+ });
it('should update an undefined version to current version', function () {
const migratedSchema = migrate(VERSION_UNDEFINED);
- expect(migratedSchema).to.eql(VERSION_1);
+ expect(migratedSchema).to.eql(VERSION_2);
});
});