-
-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(time-entity): Add time entity node
- Loading branch information
Showing
19 changed files
with
544 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <Badge text="required"/> | ||
|
||
- Type: 'listen' | 'get' | 'set' | ||
|
||
The mode of the node | ||
|
||
### Value <Badge text="required"/> | ||
|
||
- 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 | ||
|
||
<InfoPanelOnly> | ||
|
||
[link](https://zachowj.github.io/node-red-contrib-home-assistant-websocket/node/time-entity.html#examples) | ||
|
||
</InfoPanelOnly> | ||
|
||
<DocsOnly> | ||
|
||
#### Usage example | ||
|
||
![screenshot](./images/time_entity_01.png) | ||
|
||
@[code](@examples/node/time-entity/time_usage.json) | ||
|
||
</DocsOnly> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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":""}] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> { | ||
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: {}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.