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 0000000000..67f7de0e1a
Binary files /dev/null and b/docs/node/images/time_entity_01.png differ
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;
+ },
+ },
+];