From 477b3598eaf30704e8a87833850d19c8f356040f Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 10 Jul 2021 07:44:23 -0400 Subject: [PATCH] :sparkles: Add Home AssistantIO node (#1974) * :sparkles: Add Home Assistant io node * Implement continueOnFail * Add Camera Proxy resource * Clean up * Minor improvements * Remove 'Io' from the node name & code * Fix generic functions naming * :zap: Improvements * Apply review changes & fix minor bugs * Reduce nesting for additional attributes * Minor changes * :zap: Minor improvements and deactivate "Event" resource Co-authored-by: dali Co-authored-by: Jan Oberhauser --- .../HomeAssistantApi.credentials.ts | 36 ++ .../HomeAssistant/CameraProxyDescription.ts | 69 ++++ .../nodes/HomeAssistant/ConfigDescription.ts | 32 ++ .../nodes/HomeAssistant/EventDescription.ts | 144 +++++++ .../nodes/HomeAssistant/GenericFunctions.ts | 42 ++ .../nodes/HomeAssistant/HistoryDescription.ts | 128 ++++++ .../HomeAssistant/HomeAssistant.node.json | 20 + .../nodes/HomeAssistant/HomeAssistant.node.ts | 377 ++++++++++++++++++ .../nodes/HomeAssistant/LogDescription.ts | 78 ++++ .../nodes/HomeAssistant/ServiceDescription.ts | 159 ++++++++ .../nodes/HomeAssistant/StateDescription.ts | 187 +++++++++ .../HomeAssistant/TemplateDescription.ts | 52 +++ .../nodes/HomeAssistant/homeAssistant.svg | 16 + packages/nodes-base/package.json | 2 + 14 files changed, 1342 insertions(+) create mode 100644 packages/nodes-base/credentials/HomeAssistantApi.credentials.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/EventDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.json create mode 100644 packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/LogDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/StateDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/TemplateDescription.ts create mode 100644 packages/nodes-base/nodes/HomeAssistant/homeAssistant.svg diff --git a/packages/nodes-base/credentials/HomeAssistantApi.credentials.ts b/packages/nodes-base/credentials/HomeAssistantApi.credentials.ts new file mode 100644 index 0000000000..802561ba98 --- /dev/null +++ b/packages/nodes-base/credentials/HomeAssistantApi.credentials.ts @@ -0,0 +1,36 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HomeAssistantApi implements ICredentialType { + name = 'homeAssistantApi'; + displayName = 'Home Assistant API'; + documentationUrl = 'homeAssistant'; + properties = [ + { + displayName: 'Host', + name: 'host', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Port', + name: 'port', + type: 'number' as NodePropertyTypes, + default: 8123, + }, + { + displayName: 'SSL', + name: 'ssl', + type: 'boolean' as NodePropertyTypes, + default: false, + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts new file mode 100644 index 0000000000..35764bfc2a --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts @@ -0,0 +1,69 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const cameraProxyOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'cameraProxy', + ], + }, + }, + options: [ + { + name: 'Get Screenshot', + value: 'getScreenshot', + description: 'Get the camera screenshot', + }, + ], + default: 'getScreenshot', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const cameraProxyFields = [ + /* -------------------------------------------------------------------------- */ + /* cameraProxy:getScreenshot */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Camera Entity ID', + name: 'cameraEntityId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getScreenshot', + ], + resource: [ + 'cameraProxy', + ], + }, + }, + description: 'The camera entity ID.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'getScreenshot', + ], + resource: [ + 'cameraProxy', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read file.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts b/packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts new file mode 100644 index 0000000000..1606ab4df5 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/ConfigDescription.ts @@ -0,0 +1,32 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const configOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'config', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get the configuration', + }, + { + name: 'Check Configuration', + value: 'check', + description: 'Check the configuration', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/EventDescription.ts b/packages/nodes-base/nodes/HomeAssistant/EventDescription.ts new file mode 100644 index 0000000000..8a5dfb76f9 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/EventDescription.ts @@ -0,0 +1,144 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all events', + }, + { + name: 'Post', + value: 'post', + description: 'Post an event', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* event:post */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event Type', + name: 'eventType', + type: 'string', + displayOptions: { + show: { + operation: [ + 'post', + ], + resource: [ + 'event', + ], + }, + }, + required: true, + default: '', + description: 'The Entity ID for which an event will be created.', + }, + { + displayName: 'Event Attributes', + name: 'eventAttributes', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attribute', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'post', + ], + }, + }, + options: [ + { + displayName: 'Attributes', + name: 'attributes', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the attribute.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute.', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts b/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts new file mode 100644 index 0000000000..a4e6b3f9d5 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts @@ -0,0 +1,42 @@ +import { + OptionsWithUri +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +export async function homeAssistantApiRequest(this: IExecuteFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) { + const credentials = this.getCredentials('homeAssistantApi'); + + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + method, + qs, + body, + uri: uri ?? `${credentials.ssl === true ? 'https' : 'http'}://${credentials.host}:${credentials.port}/api${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts b/packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts new file mode 100644 index 0000000000..53b466d6e4 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/HistoryDescription.ts @@ -0,0 +1,128 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const historyOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'history', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all state changes', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const historyFields = [ + /* -------------------------------------------------------------------------- */ + /* history:getLogbookEntries */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'history', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'history', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'history', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + default: '', + description: 'The end of the period.', + }, + { + displayName: 'Entity IDs', + name: 'entityIds', + type: 'string', + default: '', + description: 'The entities IDs separated by comma.', + }, + { + displayName: 'Minimal Response', + name: 'minimalResponse', + type: 'boolean', + default: false, + description: 'To only return last_changed and state for states.', + }, + { + displayName: 'Significant Changes Only', + name: 'significantChangesOnly', + type: 'boolean', + default: false, + description: 'Only return significant state changes.', + }, + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'The beginning of the period.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.json b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.json new file mode 100644 index 0000000000..d5b99fa9af --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.homeAssistant", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Miscellaneous" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/homeAssistant" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.homeAssistant/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts new file mode 100644 index 0000000000..e8508685e9 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts @@ -0,0 +1,377 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + configOperations, +} from './ConfigDescription'; + +import { + serviceFields, + serviceOperations, +} from './ServiceDescription'; + +import { + stateFields, + stateOperations, +} from './StateDescription'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + logFields, + logOperations, +} from './LogDescription'; + +import { + templateFields, + templateOperations, +} from './TemplateDescription'; + +import { + historyFields, + historyOperations, +} from './HistoryDescription'; + +import { + cameraProxyFields, + cameraProxyOperations, +} from './CameraProxyDescription'; + +import { + homeAssistantApiRequest, +} from './GenericFunctions'; + +export class HomeAssistant implements INodeType { + description: INodeTypeDescription = { + displayName: 'Home Assistant', + name: 'homeAssistant', + icon: 'file:homeAssistant.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Home Assistant API', + defaults: { + name: 'Home Assistant', + color: '#3578e5', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'homeAssistantApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Camera Proxy', + value: 'cameraProxy', + }, + { + name: 'Config', + value: 'config', + }, + // { + // name: 'Event', + // value: 'event', + // }, + // { + // name: 'History', + // value: 'history', + // }, + { + name: 'Log', + value: 'log', + }, + { + name: 'Service', + value: 'service', + }, + { + name: 'State', + value: 'state', + }, + { + name: 'Template', + value: 'template', + }, + ], + default: 'config', + description: 'Resource to consume.', + }, + ...cameraProxyOperations, + ...cameraProxyFields, + ...configOperations, + ...eventOperations, + ...eventFields, + ...historyOperations, + ...historyFields, + ...logOperations, + ...logFields, + ...serviceOperations, + ...serviceFields, + ...stateOperations, + ...stateFields, + ...templateOperations, + ...templateFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const qs: IDataObject = {}; + let responseData; + for (let i = 0; i < length; i++) { + try { + if (resource === 'config') { + if (operation === 'get') { + responseData = await homeAssistantApiRequest.call(this, 'GET', '/config'); + } else if (operation === 'check') { + responseData = await homeAssistantApiRequest.call(this, 'POST', '/config/core/check_config'); + } + } else if (resource === 'service') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + responseData = await homeAssistantApiRequest.call(this, 'GET', '/services') as IDataObject[]; + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + } else if (operation === 'call') { + const domain = this.getNodeParameter('domain', i) as string; + const service = this.getNodeParameter('service', i) as string; + const serviceAttributes = this.getNodeParameter('serviceAttributes', i) as { + attributes: IDataObject[], + }; + + const body: IDataObject = {}; + + if (Object.entries(serviceAttributes).length) { + if (serviceAttributes.attributes !== undefined) { + serviceAttributes.attributes.map( + attribute => { + // @ts-ignore + body[attribute.name as string] = attribute.value; + }, + ); + } + } + + responseData = await homeAssistantApiRequest.call(this, 'POST', `/services/${domain}/${service}`, body); + if (Array.isArray(responseData) && responseData.length === 0) { + responseData = {}; + } + } + } else if (resource === 'state') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + responseData = await homeAssistantApiRequest.call(this, 'GET', '/states') as IDataObject[]; + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + } else if (operation === 'get') { + const entityId = this.getNodeParameter('entityId', i) as string; + responseData = await homeAssistantApiRequest.call(this, 'GET', `/states/${entityId}`); + } else if (operation === 'upsert') { + const entityId = this.getNodeParameter('entityId', i) as string; + const state = this.getNodeParameter('state', i) as string; + const stateAttributes = this.getNodeParameter('stateAttributes', i) as { + attributes: IDataObject[], + }; + + const body = { + state, + attributes: {}, + }; + + if (Object.entries(stateAttributes).length) { + if (stateAttributes.attributes !== undefined) { + stateAttributes.attributes.map( + attribute => { + // @ts-ignore + body.attributes[attribute.name as string] = attribute.value; + }, + ); + } + } + + responseData = await homeAssistantApiRequest.call(this, 'POST', `/states/${entityId}`, body); + } + } else if (resource === 'event') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + responseData = await homeAssistantApiRequest.call(this, 'GET', '/events') as IDataObject[]; + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + } else if (operation === 'post') { + const eventType = this.getNodeParameter('eventType', i) as string; + const eventAttributes = this.getNodeParameter('eventAttributes', i) as { + attributes: IDataObject[], + }; + + const body = {}; + + if (Object.entries(eventAttributes).length) { + if (eventAttributes.attributes !== undefined) { + eventAttributes.attributes.map( + attribute => { + // @ts-ignore + body[attribute.name as string] = attribute.value; + }, + ); + } + } + + responseData = await homeAssistantApiRequest.call(this, 'POST', `/events/${eventType}`, body); + + } + } else if (resource === 'log') { + if (operation === 'getErroLogs') { + responseData = await homeAssistantApiRequest.call(this, 'GET', '/error_log'); + if (responseData) { + responseData = { + errorLog: responseData, + }; + } + } else if (operation === 'getLogbookEntries') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + let endpoint = '/logbook'; + + if (Object.entries(additionalFields).length) { + if (additionalFields.startTime) { + endpoint = `/logbook/${additionalFields.startTime}`; + } + if (additionalFields.endTime) { + qs.end_time = additionalFields.endTime; + } + if (additionalFields.entityId) { + qs.entity = additionalFields.entityId; + } + } + + responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, qs); + + } + } else if (resource === 'template') { + if (operation === 'create') { + const body = { + template: this.getNodeParameter('template', i) as string, + }; + responseData = await homeAssistantApiRequest.call(this, 'POST', '/template', body); + if (responseData) { + responseData = { renderedTemplate: responseData }; + } + } + } else if (resource === 'history') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + let endpoint = '/history/period'; + + if (Object.entries(additionalFields).length) { + if (additionalFields.startTime) { + endpoint = `/history/period/${additionalFields.startTime}`; + } + if (additionalFields.endTime) { + qs.end_time = additionalFields.endTime; + } + if (additionalFields.entityIds) { + qs.filter_entity_id = additionalFields.entityIds; + } + if (additionalFields.minimalResponse === true) { + qs.minimal_response = additionalFields.minimalResponse; + } + if (additionalFields.significantChangesOnly === true) { + qs.significant_changes_only = additionalFields.significantChangesOnly; + } + } + + responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, qs) as IDataObject[]; + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + } + } else if (resource === 'cameraProxy') { + if (operation === 'getScreenshot') { + const cameraEntityId = this.getNodeParameter('cameraEntityId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; + const endpoint = `/camera_proxy/${cameraEntityId}`; + + let mimeType: string | undefined; + + responseData = await homeAssistantApiRequest.call(this, 'GET', endpoint, {}, {}, undefined, { + encoding: null, + resolveWithFullResponse: true, + }); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (mimeType === undefined && responseData.headers['content-type']) { + mimeType = responseData.headers['content-type']; + } + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + const data = Buffer.from(responseData.body as string); + + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data, 'screenshot.jpg', mimeType); + } + } + } catch (error) { + if (this.continueOnFail()) { + if (resource === 'cameraProxy' && operation === 'get') { + items[i].json = { error: error.message }; + } else { + returnData.push({ error: error.message }); + } + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + if (resource === 'cameraProxy' && operation === 'getScreenshot') { + return this.prepareOutputData(items); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/HomeAssistant/LogDescription.ts b/packages/nodes-base/nodes/HomeAssistant/LogDescription.ts new file mode 100644 index 0000000000..ba1d5c651f --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/LogDescription.ts @@ -0,0 +1,78 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const logOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'log', + ], + }, + }, + options: [ + { + name: 'Get Error Logs', + value: 'getErroLogs', + description: 'Get a log for a specific entity', + }, + { + name: 'Get Logbook Entries', + value: 'getLogbookEntries', + description: 'Get all logs', + }, + ], + default: 'getErroLogs', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const logFields = [ + /* -------------------------------------------------------------------------- */ + /* log:getLogbookEntries */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'log', + ], + operation: [ + 'getLogbookEntries', + ], + }, + }, + options: [ + { + displayName: 'End Time', + name: 'endTime', + type: 'dateTime', + default: '', + description: 'The end of the period.', + }, + { + displayName: 'Entity ID', + name: 'entityId', + type: 'string', + default: '', + description: 'The entity ID.', + }, + { + displayName: 'Start Time', + name: 'startTime', + type: 'dateTime', + default: '', + description: 'The beginning of the period.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts b/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts new file mode 100644 index 0000000000..d278d47227 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts @@ -0,0 +1,159 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const serviceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'service', + ], + }, + }, + options: [ + { + name: 'Call', + value: 'call', + description: 'Call a service within a specific domain', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all services', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const serviceFields = [ + /* -------------------------------------------------------------------------- */ + /* service:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'service', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'service', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* service:Call */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'service', + ], + operation: [ + 'call', + ], + }, + }, + }, + { + displayName: 'Service', + name: 'service', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'service', + ], + operation: [ + 'call', + ], + }, + }, + }, + { + displayName: 'Service Attributes', + name: 'serviceAttributes', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attribute', + default: {}, + displayOptions: { + show: { + resource: [ + 'service', + ], + operation: [ + 'call', + ], + }, + }, + options: [ + { + name: 'attributes', + displayName: 'Attributes', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the field.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the field.', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts b/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts new file mode 100644 index 0000000000..aa356f1c7a --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts @@ -0,0 +1,187 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const stateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'state', + ], + }, + }, + options: [ + { + name: 'Create or update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + }, + { + name: 'Get', + value: 'get', + description: 'Get a state for a specific entity', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all states', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const stateFields = [ + /* -------------------------------------------------------------------------- */ + /* state:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Entity ID', + name: 'entityId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'state', + ], + }, + }, + required: true, + default: '', + description: 'The entity ID.', + }, + + /* -------------------------------------------------------------------------- */ + /* state:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'state', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'state', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* state:upsert */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Entity ID', + name: 'entityId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'upsert', + ], + resource: [ + 'state', + ], + }, + }, + required: true, + default: '', + description: 'The entity ID for which a state will be created.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'state', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + { + displayName: 'State Attributes', + name: 'stateAttributes', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attribute', + default: {}, + displayOptions: { + show: { + resource: [ + 'state', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Attributes', + name: 'attributes', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the attribute.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the attribute.', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/TemplateDescription.ts b/packages/nodes-base/nodes/HomeAssistant/TemplateDescription.ts new file mode 100644 index 0000000000..9f35155c6a --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/TemplateDescription.ts @@ -0,0 +1,52 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const templateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'template', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'create a template', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const templateFields = [ + + /* -------------------------------------------------------------------------- */ + /* template:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Template', + name: 'template', + type: 'string', + displayOptions: { + show: { + resource: [ + 'template', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + default: '', + description: 'Render a Home Assistant template. See template docs for more information.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/HomeAssistant/homeAssistant.svg b/packages/nodes-base/nodes/HomeAssistant/homeAssistant.svg new file mode 100644 index 0000000000..9e25b23da5 --- /dev/null +++ b/packages/nodes-base/nodes/HomeAssistant/homeAssistant.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 4d7363c412..d4fbd2eba1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -119,6 +119,7 @@ "dist/credentials/HarvestApi.credentials.js", "dist/credentials/HarvestOAuth2Api.credentials.js", "dist/credentials/HelpScoutOAuth2Api.credentials.js", + "dist/credentials/HomeAssistantApi.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -403,6 +404,7 @@ "dist/nodes/Harvest/Harvest.node.js", "dist/nodes/HelpScout/HelpScout.node.js", "dist/nodes/HelpScout/HelpScoutTrigger.node.js", + "dist/nodes/HomeAssistant/HomeAssistant.node.js", "dist/nodes/HtmlExtract/HtmlExtract.node.js", "dist/nodes/HttpRequest.node.js", "dist/nodes/Hubspot/Hubspot.node.js",