From ae190fd1561bd9affb516c1f5f7ac262b5be8cad Mon Sep 17 00:00:00 2001 From: Jason <37859597+zachowj@users.noreply.github.com> Date: Sun, 16 Jul 2023 18:22:23 -0700 Subject: [PATCH] feat(time-entity): Add time entity node --- docs/.vuepress/config.ts | 1 + docs/node/images/time_entity_01.png | Bin 0 -> 35523 bytes docs/node/time-entity.md | 60 ++++++ examples/node/time-entity/time_usage.json | 1 + gulpfile.js | 1 + src/const.ts | 2 + src/editor.ts | 2 + src/editor/exposenode.ts | 10 + src/index.ts | 2 + src/nodes/entity-config/editor.html | 4 + src/nodes/entity-config/editor/editor.ts | 2 +- src/nodes/entity-config/index.ts | 3 +- src/nodes/entity-config/locale.json | 3 +- src/nodes/time-entity/TimeEntityController.ts | 172 ++++++++++++++++++ src/nodes/time-entity/editor.html | 40 ++++ src/nodes/time-entity/editor.ts | 110 +++++++++++ src/nodes/time-entity/index.ts | 105 +++++++++++ src/nodes/time-entity/locale.json | 17 ++ src/nodes/time-entity/migrations.ts | 12 ++ 19 files changed, 544 insertions(+), 3 deletions(-) create mode 100644 docs/node/images/time_entity_01.png create mode 100644 docs/node/time-entity.md create mode 100644 examples/node/time-entity/time_usage.json create mode 100644 src/nodes/time-entity/TimeEntityController.ts create mode 100644 src/nodes/time-entity/editor.html create mode 100644 src/nodes/time-entity/editor.ts create mode 100644 src/nodes/time-entity/index.ts create mode 100644 src/nodes/time-entity/locale.json create mode 100644 src/nodes/time-entity/migrations.ts diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index e4995d5322..7805e2f5ed 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -147,6 +147,7 @@ export default defineUserConfig({ 'sensor', 'switch', 'text', + { text: 'Time', link: 'time-entity' }, 'update-config', ], }, diff --git a/docs/node/images/time_entity_01.png b/docs/node/images/time_entity_01.png new file mode 100644 index 0000000000000000000000000000000000000000..67f7de0e1a08534ed9e8e6cd987ef3a5faa97d92 GIT binary patch literal 35523 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 S#T$MBf7Dd8l;0?t`TZZzTtJlo literal 0 HcmV?d00001 diff --git a/docs/node/time-entity.md b/docs/node/time-entity.md new file mode 100644 index 0000000000..9d3732ee18 --- /dev/null +++ b/docs/node/time-entity.md @@ -0,0 +1,60 @@ +::: warning +_Needs [Custom Integration](https://github.com/zachowj/hass-node-red) installed +in Home Assistant for this node to function_ +::: + +# Time + +Creates a time entity in Home Assistant which can be manipulated from this node or Home Assistant. + +## Configuration + +### Mode + +- Type: 'listen' | 'get' | 'set' + +The mode of the node + +### Value + +- Type: `string` +- Format: `HH:mm:ss` | `HH:mm` + +The value of the entity should be updated to + +## Inputs + +properties of `msg.payload` + +### value + +- Type: `string` +- Format: `HH:mm:ss` | `HH:mm` + +The value of the entity should be updated to + +## Outputs + +Value types: + +- `value`: The value of the entity +- `previous value`: The previous value of the entity +- `config`: The config properties of the node + +## Examples + + + +[link](https://zachowj.github.io/node-red-contrib-home-assistant-websocket/node/time-entity.html#examples) + + + + + +#### Usage example + +![screenshot](./images/time_entity_01.png) + +@[code](@examples/node/time-entity/time_usage.json) + + diff --git a/examples/node/time-entity/time_usage.json b/examples/node/time-entity/time_usage.json new file mode 100644 index 0000000000..3402dd8ab5 --- /dev/null +++ b/examples/node/time-entity/time_usage.json @@ -0,0 +1 @@ +[{"id":"5a3098079cfdd280","type":"server-state-changed","z":"7f704f92ee3c3f87","name":"","server":"","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"time.time_test","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"}],"x":216,"y":624,"wires":[["af4b36c8422aec66"]]},{"id":"af4b36c8422aec66","type":"debug","z":"7f704f92ee3c3f87","name":"event state","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.old_state.state & \" : \" & payload.new_state.state","targetType":"jsonata","statusVal":"","statusType":"auto","x":522,"y":624,"wires":[]},{"id":"2f80e30a8dc150d8","type":"debug","z":"7f704f92ee3c3f87","name":"time listen","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"previousValue & \" : \" & payload ","targetType":"jsonata","statusVal":"","statusType":"auto","x":508,"y":576,"wires":[]},{"id":"1a40e5a4d0f530d2","type":"inject","z":"7f704f92ee3c3f87","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":154,"y":336,"wires":[["621d17a1e3fbb9a5"]]},{"id":"4c727df1bc583e44","type":"api-call-service","z":"7f704f92ee3c3f87","name":"","server":"","version":5,"debugenabled":false,"domain":"time","service":"set_value","areaId":[],"deviceId":[],"entityId":["time.time_test"],"data":"{\"time\": payload}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":528,"y":480,"wires":[[]]},{"id":"2a4a0d5fc9bb8d86","type":"debug","z":"7f704f92ee3c3f87","name":"time set","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"previousValue & \" : \" & payload ","targetType":"jsonata","statusVal":"","statusType":"auto","x":636,"y":336,"wires":[]},{"id":"f3b9c9fb22295113","type":"inject","z":"7f704f92ee3c3f87","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":154,"y":528,"wires":[["d5731b07e4893926"]]},{"id":"276afdfb58fe76f1","type":"debug","z":"7f704f92ee3c3f87","name":"time get","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":508,"y":528,"wires":[]},{"id":"8954ce09cca90f63","type":"ha-time-entity","z":"7f704f92ee3c3f87","name":"listen time","version":0,"debugenabled":false,"inputs":0,"outputs":1,"entityConfig":"fa434795e5d1be74","mode":"listen","value":"payload","valueType":"msg","outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"value"},{"property":"previousValue","propertyType":"msg","value":"","valueType":"previousValue"}],"x":156,"y":576,"wires":[["2f80e30a8dc150d8"]]},{"id":"d5731b07e4893926","type":"ha-time-entity","z":"7f704f92ee3c3f87","name":"get time","version":0,"debugenabled":false,"inputs":1,"outputs":1,"entityConfig":"fa434795e5d1be74","mode":"get","value":"payload","valueType":"msg","outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"value"}],"x":290,"y":528,"wires":[["276afdfb58fe76f1"]]},{"id":"fa09042625445217","type":"ha-time-entity","z":"7f704f92ee3c3f87","name":"set time","version":0,"debugenabled":false,"inputs":1,"outputs":1,"entityConfig":"fa434795e5d1be74","mode":"set","value":"payload","valueType":"msg","outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"value"},{"property":"previousValue","propertyType":"msg","value":"","valueType":"previousValue"}],"x":494,"y":336,"wires":[["2a4a0d5fc9bb8d86"]]},{"id":"458c8d233a9d18af","type":"inject","z":"7f704f92ee3c3f87","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":155,"y":288,"wires":[["74d5dc5ff4d3c0cb"]]},{"id":"9c23cc9f504fa4a1","type":"link in","z":"7f704f92ee3c3f87","name":"HH:MM:SS","links":[],"x":156,"y":192,"wires":[["7e92542f4ff33de0"]],"l":true},{"id":"cd50b5472f373ad6","type":"link in","z":"7f704f92ee3c3f87","name":"HH:MM","links":[],"x":146,"y":240,"wires":[["801e1e33127e8d4c"]],"l":true},{"id":"7e92542f4ff33de0","type":"function","z":"7f704f92ee3c3f87","name":"generate time HH:MM:SS","func":"const randomHour = Math.floor(Math.random() * 24);\nconst randomMinute = Math.floor(Math.random() * 60);\nconst randomSecond = Math.floor(Math.random() * 60);\n\nmsg.payload = `${randomHour}:${randomMinute\n .toString()\n .padStart(2, '0')}:${randomSecond.toString().padStart(2, '0')}`;\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":351,"y":192,"wires":[["1b78615c029b6dab"]]},{"id":"801e1e33127e8d4c","type":"function","z":"7f704f92ee3c3f87","name":"generate time HH:MM","func":"const randomHour = Math.floor(Math.random() * 24);\nconst randomMinute = Math.floor(Math.random() * 60);\n\nmsg.payload = `${randomHour}:${randomMinute\n .toString()\n .padStart(2, '0')}`;\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":341,"y":240,"wires":[["cc16260a68095e77"]]},{"id":"1b78615c029b6dab","type":"link out","z":"7f704f92ee3c3f87","name":"link out 1","mode":"return","links":[],"x":519,"y":192,"wires":[]},{"id":"cc16260a68095e77","type":"link out","z":"7f704f92ee3c3f87","name":"link out 2","mode":"return","links":[],"x":519,"y":240,"wires":[]},{"id":"74d5dc5ff4d3c0cb","type":"link call","z":"7f704f92ee3c3f87","name":"","links":["9c23cc9f504fa4a1"],"linkType":"static","timeout":"30","x":310,"y":288,"wires":[["fa09042625445217"]]},{"id":"621d17a1e3fbb9a5","type":"link call","z":"7f704f92ee3c3f87","name":"","links":["cd50b5472f373ad6"],"linkType":"static","timeout":"30","x":300,"y":336,"wires":[["fa09042625445217"]]},{"id":"decebfa96ff2b375","type":"inject","z":"7f704f92ee3c3f87","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":154,"y":480,"wires":[["5e44eb1c1de80d82"]]},{"id":"34e9926f8d967e47","type":"inject","z":"7f704f92ee3c3f87","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":154,"y":432,"wires":[["e711fd4eb0f21d6f"]]},{"id":"e711fd4eb0f21d6f","type":"link call","z":"7f704f92ee3c3f87","name":"","links":["9c23cc9f504fa4a1"],"linkType":"static","timeout":"30","x":310,"y":432,"wires":[["4c727df1bc583e44"]]},{"id":"5e44eb1c1de80d82","type":"link call","z":"7f704f92ee3c3f87","name":"","links":["cd50b5472f373ad6"],"linkType":"static","timeout":"30","x":300,"y":480,"wires":[["4c727df1bc583e44"]]},{"id":"861b8a79994a76cd","type":"inject","z":"7f704f92ee3c3f87","name":"invalid","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"invalid","payloadType":"str","x":153,"y":384,"wires":[["fa09042625445217"]]},{"id":"fa434795e5d1be74","type":"ha-entity-config","server":"bf5874816710d0c7","deviceConfig":"65bf2a1a7e89a8d9","name":"time test","version":"6","entityType":"time","haConfig":[{"property":"name","value":"time test"},{"property":"icon","value":""},{"property":"entity_category","value":""},{"property":"entity_picture","value":""}],"resend":false,"debugEnabled":false},{"id":"65bf2a1a7e89a8d9","type":"ha-device-config","name":"test device","hwVersion":"","manufacturer":"Node-RED","model":"","swVersion":""}] diff --git a/gulpfile.js b/gulpfile.js index ac41be1321..c650b2d8a8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -87,6 +87,7 @@ const nodeMap = { tag: { doc: 'tag', type: 'ha-tag' }, text: { doc: 'text', type: 'ha-text' }, time: { doc: 'time', type: 'ha-time' }, + 'time-entity': { doc: 'time-entity', type: 'ha-time-entity' }, 'trigger-state': { doc: 'trigger-state', type: 'trigger-state' }, 'update-config': { doc: 'update-config', type: 'ha-update-config' }, 'wait-until': { doc: 'wait-until', type: 'ha-wait-until' }, diff --git a/src/const.ts b/src/const.ts index 97d3664401..dcffa4d38f 100644 --- a/src/const.ts +++ b/src/const.ts @@ -49,6 +49,7 @@ export enum EntityType { Sensor = 'sensor', Switch = 'switch', Text = 'text', + Time = 'time', } export enum EntityFilterType { @@ -90,6 +91,7 @@ export enum NodeType { Sensor = 'ha-sensor', Switch = 'ha-switch', Text = 'ha-text', + TimeEntity = 'ha-time-entity', UpdateConfig = 'ha-update-config', } diff --git a/src/editor.ts b/src/editor.ts index 276a87bccc..892eb7cca8 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -43,6 +43,7 @@ import SwitchEditor from './nodes/switch/editor'; import TagEditor from './nodes/tag/editor'; import TextEditor from './nodes/text/editor'; import TimeEditor from './nodes/time/editor'; +import TimeEntityEditor from './nodes/time-entity/editor'; import TriggerStateEditor from './nodes/trigger-state/editor'; import UpdateConfigEditor from './nodes/update-config/editor'; import WaitUntilEditor from './nodes/wait-until/editor'; @@ -99,4 +100,5 @@ RED.nodes.registerType(NodeType.Select, SelectEditor); RED.nodes.registerType(NodeType.Sensor, SensorEditor); RED.nodes.registerType(NodeType.Switch, SwitchEditor); RED.nodes.registerType(NodeType.Text, TextEditor); +RED.nodes.registerType(NodeType.TimeEntity, TimeEntityEditor); RED.nodes.registerType(NodeType.UpdateConfig, UpdateConfigEditor); diff --git a/src/editor/exposenode.ts b/src/editor/exposenode.ts index bfff0f17c1..c29c40d444 100644 --- a/src/editor/exposenode.ts +++ b/src/editor/exposenode.ts @@ -91,6 +91,11 @@ export function init(n: HassNodeProperties) { renderAlert('1.3.0'); } break; + case NodeType.TimeEntity: + if ($('#node-input-entityConfig').val() !== '_ADD_') { + renderAlert('2.1.0'); + } + break; case NodeType.Select: if ($('#node-input-entityConfig').val() !== '_ADD_') { renderAlert('1.4.0'); @@ -146,6 +151,11 @@ function render() { renderAlert('2.0.0'); } break; + case NodeType.TimeEntity: + if ($('#node-input-entityConfig').val() !== '_ADD_') { + renderAlert('2.1.0'); + } + break; case NodeType.Webhook: if ($('#node-input-server').val() !== '_ADD_') { renderAlert('1.6.0'); diff --git a/src/index.ts b/src/index.ts index 73402ec6bd..bdd448e029 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import switchNode from './nodes/switch'; import tagNode from './nodes/tag'; import textNode from './nodes/text'; import timeNode from './nodes/time'; +import timeEntityNode from './nodes/time-entity'; import triggerStateNode from './nodes/trigger-state'; import updateConfigNode from './nodes/update-config'; import waitUntilNode from './nodes/wait-until'; @@ -70,6 +71,7 @@ const nodes: Record = { [NodeType.Sensor]: sensorNode, [NodeType.Switch]: switchNode, [NodeType.Text]: textNode, + [NodeType.TimeEntity]: timeEntityNode, [NodeType.UpdateConfig]: updateConfigNode, }; diff --git a/src/nodes/entity-config/editor.html b/src/nodes/entity-config/editor.html index 4cf4a3fb9d..a131d3947d 100644 --- a/src/nodes/entity-config/editor.html +++ b/src/nodes/entity-config/editor.html @@ -54,6 +54,10 @@ value="text" data-i18n="ha-entity-config.label.type_option.text" > + diff --git a/src/nodes/entity-config/editor/editor.ts b/src/nodes/entity-config/editor/editor.ts index 1b604f9b4a..44be5b12fd 100644 --- a/src/nodes/entity-config/editor/editor.ts +++ b/src/nodes/entity-config/editor/editor.ts @@ -107,7 +107,7 @@ const EntityConfigEditor: EditorNodeDef = { const mergedOptions: HaConfigOption[] = [ ...defaultHaConfigOptions, - ...haConfigOptions[value], + ...(haConfigOptions[value] ?? []), ]; mergedOptions.forEach((o) => { const val = diff --git a/src/nodes/entity-config/index.ts b/src/nodes/entity-config/index.ts index d7b156ad39..e5d98acf74 100644 --- a/src/nodes/entity-config/index.ts +++ b/src/nodes/entity-config/index.ts @@ -67,7 +67,8 @@ export default function entityConfigNode( } case EntityType.Number: case EntityType.Select: - case EntityType.Text: { + case EntityType.Text: + case EntityType.Time: { this.integration = new ValueEntityIntegration(props); break; } diff --git a/src/nodes/entity-config/locale.json b/src/nodes/entity-config/locale.json index 39096e8c72..1de71dc5f8 100644 --- a/src/nodes/entity-config/locale.json +++ b/src/nodes/entity-config/locale.json @@ -14,7 +14,8 @@ "select": "select", "sensor": "sensor", "switch": "switch", - "text": "text" + "text": "text", + "time": "time" } } } diff --git a/src/nodes/time-entity/TimeEntityController.ts b/src/nodes/time-entity/TimeEntityController.ts new file mode 100644 index 0000000000..63fd1f4a83 --- /dev/null +++ b/src/nodes/time-entity/TimeEntityController.ts @@ -0,0 +1,172 @@ +import { NodeMessage } from 'node-red'; + +import InputOutputController, { + InputOutputControllerOptions, + InputProperties, +} from '../../common/controllers/InputOutputController'; +import InputError from '../../common/errors/InputError'; +import NoConnectionError from '../../common/errors/NoConnectionError'; +import { IntegrationEvent } from '../../common/integration/Integration'; +import ValueEntityIntegration from '../../common/integration/ValueEntityIntegration'; +import { ValueIntegrationMode } from '../../const'; +import { EntityConfigNode } from '../entity-config'; +import { TimeEntityNode, TimeEntityNodeProperties } from '.'; + +type TimeEntityControllerConstructor = InputOutputControllerOptions< + TimeEntityNode, + TimeEntityNodeProperties +>; + +export default class TimeEntityController extends InputOutputController< + TimeEntityNode, + TimeEntityNodeProperties +> { + protected integration?: ValueEntityIntegration; + #entityConfigNode?: EntityConfigNode; + + constructor(props: TimeEntityControllerConstructor) { + super(props); + this.#entityConfigNode = this.integration?.getEntityConfigNode(); + } + + // Handles input messages when the node is in "get" mode + async #onInputModeGet({ done, message, send }: InputProperties) { + const value = this.#entityConfigNode?.state?.getLastPayload()?.state as + | string + | undefined; + + this.status.setSuccess(value); + this.setCustomOutputs(this.node.config.outputProperties, message, { + config: this.node.config, + value, + }); + + send(message); + done(); + } + + // Handles input messages when the node is in "set" mode + async #onInputModeSet({ + done, + message, + parsedMessage, + send, + }: InputProperties) { + if (!this.integration?.isConnected) { + throw new NoConnectionError(); + } + if (!this.integration?.isIntegrationLoaded) { + throw new InputError( + 'home-assistant.error.integration_not_loaded', + 'home-assistant.status.error' + ); + } + + const value = this.typedInputService.getValue( + parsedMessage.value.value, + parsedMessage.valueType.value, + { + message, + } + ); + + // get previous value before updating + const previousValue = this.#entityConfigNode?.state?.getLastPayload() + ?.state as string | undefined; + await this.#prepareSend(message, value); + // send value change to all time nodes + this.#entityConfigNode?.emit( + IntegrationEvent.ValueChange, + value, + previousValue + ); + + send(message); + done(); + } + + protected async onInput({ + done, + message, + parsedMessage, + send, + }: InputProperties) { + if (this.node.config.mode === ValueIntegrationMode.Get) { + this.#onInputModeGet({ done, message, parsedMessage, send }); + } else if (this.node.config.mode === ValueIntegrationMode.Set) { + await this.#onInputModeSet({ done, message, parsedMessage, send }); + } else { + throw new InputError( + 'ha-text.error.mode_not_supported', + 'home-assistant.status.error' + ); + } + } + + // Triggers when a entity value changes in Home Assistant + public async onValueChange(value: string, previousValue?: string) { + const message: NodeMessage = {}; + await this.#prepareSend(message, value, previousValue); + + this.node.send(message); + } + + /** + * Checks if the given time string is in the format "HH:mm:ss" or "HH:mm". + * @param text The time string to check. + * @returns True if the time string is in the correct format, false otherwise. + */ + #isValidValue(text: string): boolean { + const pattern = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/; + const regex = new RegExp(pattern); + return regex.test(text); + + return true; + } + + /** + * Formats the given time string to the format "HH:mm:ss". + * If the seconds are not provided, it defaults to "00". + * @param text The time string to format. + * @returns The formatted time string. + */ + #getFormattedValue(text: string): string { + const [hours, minutes, seconds] = text.split(':'); + + return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:${ + seconds?.padStart(2, '0') ?? '00' + }`; + } + + // Take care of repetative code in onInput and onValueChange + async #prepareSend( + message: NodeMessage, + value: string, + previousValue?: string + ): Promise { + if (this.#isValidValue(value) === false) { + throw new InputError( + 'ha-time-entity.error.invalid_format', + 'home-assistant.status.error' + ); + } + + value = this.#getFormattedValue(value); + + await this.integration?.updateHomeAssistant(value); + this.status.setSuccess(value); + if (!previousValue) { + previousValue = this.#entityConfigNode?.state?.getLastPayload() + ?.state as string | undefined; + } + this.setCustomOutputs(this.node.config.outputProperties, message, { + config: this.node.config, + value, + previousValue, + }); + this.#entityConfigNode?.state?.setLastPayload({ + state: value, + attributes: {}, + }); + } +} diff --git a/src/nodes/time-entity/editor.html b/src/nodes/time-entity/editor.html new file mode 100644 index 0000000000..b078d97b9a --- /dev/null +++ b/src/nodes/time-entity/editor.html @@ -0,0 +1,40 @@ + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
    diff --git a/src/nodes/time-entity/editor.ts b/src/nodes/time-entity/editor.ts new file mode 100644 index 0000000000..ff43ebadd4 --- /dev/null +++ b/src/nodes/time-entity/editor.ts @@ -0,0 +1,110 @@ +import { EditorNodeDef, EditorNodeProperties, EditorRED } from 'node-red'; + +import { + EntityType, + NodeType, + TypedInputTypes, + ValueIntegrationMode, +} from '../../const'; +import * as haOutputs from '../../editor/components/output-properties'; +import * as exposeNode from '../../editor/exposenode'; +import ha, { NodeCategory, NodeColor } from '../../editor/ha'; +import { OutputProperty } from '../../editor/types'; +import { saveEntityType } from '../entity-config/editor/helpers'; + +declare const RED: EditorRED; + +interface TimeEntityEditorNodeProperties extends EditorNodeProperties { + version: number; + debugenabled: boolean; + entityConfig: any; + mode: ValueIntegrationMode; + value: string; + outputProperties: OutputProperty[]; +} + +const TimeEntityEditor: EditorNodeDef = { + category: NodeCategory.HomeAssistantEntities, + color: NodeColor.Beta, + inputs: 0, + outputs: 1, + icon: 'font-awesome/fa-clock-o', + align: 'left', + paletteLabel: 'time', + label: function () { + return this.name || 'time'; + }, + labelStyle: ha.labelStyle, + defaults: { + name: { value: '' }, + version: { value: RED.settings.get('haTimeEntityVersion', 0) }, + debugenabled: { value: false }, + // @ts-expect-error - DefinitelyTyped is wrong inputs can be changed + inputs: { value: 0 }, + outputs: { value: 1 }, + entityConfig: { + value: '', + type: NodeType.EntityConfig, + // @ts-expect-error - DefinitelyTyped is missing this property + filter: (config) => config.entityType === 'time', + required: true, + }, + mode: { value: ValueIntegrationMode.Listen }, + value: { value: 'payload' }, + valueType: { value: TypedInputTypes.Message }, + outputProperties: { + value: [ + { + property: 'payload', + propertyType: TypedInputTypes.Message, + value: '', + valueType: TypedInputTypes.Value, + }, + { + property: 'previousValue', + propertyType: TypedInputTypes.Message, + value: '', + valueType: TypedInputTypes.PreviousValue, + }, + ], + validate: haOutputs.validate, + }, + }, + oneditprepare: function () { + ha.setup(this); + exposeNode.init(this); + + saveEntityType(EntityType.Time); + $('#dialog-form').prepend(ha.betaWarning(988)); + + const $valueRow = $('#node-input-value').parent(); + $('#node-input-mode').on('change', function (this: HTMLSelectElement) { + $valueRow.toggle(this.value === ValueIntegrationMode.Set); + $('#node-input-inputs').val( + this.value === ValueIntegrationMode.Listen ? 0 : 1 + ); + }); + + $('#node-input-value').typedInput({ + types: [ + TypedInputTypes.Message, + TypedInputTypes.Flow, + TypedInputTypes.Global, + TypedInputTypes.JSONata, + TypedInputTypes.String, + ], + typeField: '#node-input-valueType', + // @ts-expect-error - DefinitelyTyped is wrong typedInput can take a object as a parameter + type: this.valueType, + }); + + haOutputs.createOutputs(this.outputProperties, { + extraTypes: [TypedInputTypes.Value, TypedInputTypes.PreviousValue], + }); + }, + oneditsave: function () { + this.outputProperties = haOutputs.getOutputs(); + }, +}; + +export default TimeEntityEditor; diff --git a/src/nodes/time-entity/index.ts b/src/nodes/time-entity/index.ts new file mode 100644 index 0000000000..42c33fd15e --- /dev/null +++ b/src/nodes/time-entity/index.ts @@ -0,0 +1,105 @@ +import Joi from 'joi'; + +import { createControllerDependencies } from '../../common/controllers/helpers'; +import Events from '../../common/events/Events'; +import { IntegrationEvent } from '../../common/integration/Integration'; +import InputService, { NodeInputs } from '../../common/services/InputService'; +import State from '../../common/State'; +import Status from '../../common/status/Status'; +import { TypedInputTypes, ValueIntegrationMode } from '../../const'; +import { RED } from '../../globals'; +import { migrate } from '../../helpers/migrate'; +import { getConfigNodes } from '../../helpers/node'; +import { getHomeAssistant } from '../../homeAssistant/index'; +import { + BaseNode, + EntityBaseNodeProperties, + OutputProperty, +} from '../../types/nodes'; +import TimeEntityController from './TimeEntityController'; + +export interface TimeEntityNodeProperties extends EntityBaseNodeProperties { + mode: ValueIntegrationMode; + value: string; + valueType: string; + outputProperties: OutputProperty[]; +} + +export interface TimeEntityNode extends BaseNode { + config: TimeEntityNodeProperties; +} + +export const inputs: NodeInputs = { + value: { + messageProp: 'payload.value', + configProp: 'value', + default: 'payload', + }, + valueType: { + messageProp: 'payload.valueType', + configProp: 'valueType', + default: TypedInputTypes.Message, + }, +}; + +export const inputSchema: Joi.ObjectSchema = Joi.object({ + value: Joi.string().required(), + valueType: Joi.string() + .valid( + TypedInputTypes.Message, + TypedInputTypes.Flow, + TypedInputTypes.Global, + TypedInputTypes.JSONata, + TypedInputTypes.String + ) + .required(), +}); + +export default function timeEntityNode( + this: TimeEntityNode, + config: TimeEntityNodeProperties +) { + RED.nodes.createNode(this, config); + this.config = migrate(config); + + const { entityConfigNode, serverConfigNode } = getConfigNodes(this); + const homeAssistant = getHomeAssistant(serverConfigNode); + 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 inputService = new InputService({ + inputs, + nodeConfig: this.config, + schema: inputSchema, + }); + + entityConfigNode.integration.setStatus(status); + const controller = new TimeEntityController({ + inputService, + integration: entityConfigNode.integration, + node: this, + status, + ...controllerDeps, + state, + }); + + if (this.config.mode === ValueIntegrationMode.Listen) { + const entityConfigEvents = new Events({ + node: this, + emitter: entityConfigNode, + }); + + entityConfigEvents.addListener( + IntegrationEvent.ValueChange, + controller.onValueChange.bind(controller) + ); + } +} diff --git a/src/nodes/time-entity/locale.json b/src/nodes/time-entity/locale.json new file mode 100644 index 0000000000..1af11f97e5 --- /dev/null +++ b/src/nodes/time-entity/locale.json @@ -0,0 +1,17 @@ +{ + "ha-time-entity": { + "error": { + "invalid_format": "Time value must be in 24-hour format HH:MM[:SS]" + }, + "label": { + "entity_config": "Entity config", + "mode": "Mode", + "mode_option": { + "in": "listen for changes", + "out": "set value" + }, + "name": "Name", + "value": "Value" + } + } +} diff --git a/src/nodes/time-entity/migrations.ts b/src/nodes/time-entity/migrations.ts new file mode 100644 index 0000000000..f9738460c4 --- /dev/null +++ b/src/nodes/time-entity/migrations.ts @@ -0,0 +1,12 @@ +export default [ + { + version: 0, + up: (schema: any) => { + const newSchema = { + ...schema, + version: 0, + }; + return newSchema; + }, + }, +];