node-red-dashboard-2-t86
Version:
Set of Node-RED nodes to controll home automation based on Unipi Patron and DALI.
336 lines (276 loc) • 9.51 kB
text/typescript
import { Node, NodeAPI, NodeDef } from "node-red";
import { typedStrToValue, ValueType } from "../editor-prase-helper";
interface MultiButtonNodeConfig extends NodeDef {
name: string,
shortpresslimitms: string,
nextpresswaitms: string,
inputkeypath: string,
inputkeypathType: 'msg',
pushvalue: string,
pushvalueType: ValueType,
releasevalue: string,
releasevalueType: ValueType,
shortenabled: boolean,
dblshortenabled: boolean,
longenabled: boolean,
shortlongenabled: boolean,
warn: boolean,
debug: boolean
}
enum InputEvent {
Push = 'in.push',
Release = 'in.release',
Invalid = 'in.invalid'
}
enum OutputEvent {
ShortPress = 'out.short',
DoubleShortPress = 'out.dblshort',
LongPressStart = 'out.long.start',
ShortLongPressStart = 'out.shortlong.start',
LongPressEnd = 'out.long.end',
ShortLongPressEnd = 'out.shortlong.end',
}
enum State {
Ready = 'stat.ready',
PushedOnce = 'stat.pushedOnce',
LongInProgress = 'stat.longInProgress',
WaitingAnotherPush = 'stat.waitingAnotherPush',
PushedTwice = 'stat.pushedTwice',
ShortLongInProgress = 'stat.shortLongInProgress'
}
type ParsedConfig = {
shortPressLimitMs: number,
nextPressWaitMs: number,
inputKeyPath: string,
pushValue: string | number | boolean,
releaseValue: string | number | boolean,
enabled: {
short: boolean,
dblShort: boolean,
long: boolean,
shortLong: boolean
},
warn: boolean,
debug: boolean
}
type SendMsg = (msg: any) => void
type DoneFn = () => void
type Retained = {
send: SendMsg,
done: DoneFn,
msg: any
}
function parseEditorConfig(config: MultiButtonNodeConfig): ParsedConfig {
return {
shortPressLimitMs: parseInt(config.shortpresslimitms),
nextPressWaitMs: parseInt(config.nextpresswaitms),
inputKeyPath: config.inputkeypath,
pushValue: typedStrToValue(config.pushvalue, config.pushvalueType),
releaseValue: typedStrToValue(config.releasevalue, config.releasevalueType),
enabled: {
short: config.shortenabled,
dblShort: config.dblshortenabled,
long: config.longenabled,
shortLong: config.shortlongenabled
},
warn: config.warn,
debug: config.debug
}
}
class MultiButton {
private RED: NodeAPI
private node: Node
private pConf: ParsedConfig
private shortPushTo?: ReturnType<typeof setTimeout>
private waitAnotherPushTo?: ReturnType<typeof setTimeout>
private longPushStart?: Date
private storedSendFn?: Retained
private latestSendFn?: Retained
private state: State = State.Ready
constructor(RED: NodeAPI, node: Node, config: MultiButtonNodeConfig) {
this.RED = RED
this.node = node
RED.nodes.createNode(node, config)
this.pConf = parseEditorConfig(config)
this.node.on('input', this.onInput.bind(this))
this.node.on('close', this.onClose.bind(this))
}
private onInput(msg: any, send: SendMsg, done: () => void) {
this.latestSendFn = { send, done, msg }
const evt = this.eventWithMsg(msg)
switch (evt) {
case InputEvent.Push: this.onPush(); break;
case InputEvent.Release: this.onRelease(); break;
case InputEvent.Invalid: this.pConf.warn && this.node.warn(`Invalid event in message, ${msg}`)
}
if (done) done()
}
private onPush() {
this.debug(`onPush`)
switch (this.state) {
case State.Ready: this.goPushedOnce(); return
case State.WaitingAnotherPush: this.goPushedTwice(); return
}
this.invalidTransition('onPush')
}
private onRelease() {
this.debug(`onRelease`)
switch (this.state) {
case State.PushedOnce: this.goWaitAnotherPush(); return
case State.PushedTwice: this.finalizeDoubleShort(); return
case State.LongInProgress: this.finalizeLong(); return
case State.ShortLongInProgress: this.finalizeShortLong(); return
}
this.invalidTransition('onRelease')
}
private onShortPushTo() {
this.debug(`onShortPushTo`)
switch (this.state) {
case State.PushedOnce: this.goLongStart(); return
case State.PushedTwice: this.goShortLongStart(); return
}
this.invalidTransition('onShortPushTo')
}
private onWaitAnotherPushTo() {
this.debug(`onWaitAnotherPushTo`)
switch (this.state) {
case State.WaitingAnotherPush: this.finalizeShort(); return
}
this.invalidTransition('onWaitAnotherPushTo')
}
private onClose() {
if (this.shortPushTo) clearTimeout(this.shortPushTo)
if (this.waitAnotherPushTo) clearTimeout(this.waitAnotherPushTo)
this.shortPushTo = undefined
this.waitAnotherPushTo = undefined
}
private goPushedOnce() {
this.debug(`goPushedOnce -> State.PushedOnce\n setting short push TO`)
this.storedSend()
this.resetTimeouts()
this.shortPushTo = setTimeout(this.onShortPushTo.bind(this), this.pConf.shortPressLimitMs)
this.state = State.PushedOnce
}
private goPushedTwice() {
this.debug(`goPushedTwice -> State.PushedTwice\n setting short push TO`)
this.resetTimeouts()
this.shortPushTo = setTimeout(this.onShortPushTo.bind(this), this.pConf.shortPressLimitMs)
this.state = State.PushedTwice
}
private goWaitAnotherPush() {
this.state = State.WaitingAnotherPush
this.resetTimeouts()
// If there is no dbl press event enabled it's pointless to wait
if (!this.pConf.enabled.dblShort && !this.pConf.enabled.shortLong) {
this.debug(`goWaitAnotherPush -> State.WaitingAnotherPush\n not waiting`)
this.onWaitAnotherPushTo()
return
}
// There is dbl press event enabled. Let's wait if second push is comming
this.debug(`goWaitAnotherPush -> State.WaitingAnotherPush\n setting wait TO`)
this.waitAnotherPushTo = setTimeout(this.onWaitAnotherPushTo.bind(this), this.pConf.nextPressWaitMs)
}
private goLongStart() {
this.debug(`goLongStart -> State.LongInProgress`)
this.resetTimeouts()
this.longPushStart = new Date()
this.sendOutEvent(OutputEvent.LongPressStart)
this.storedSend()
this.state = State.LongInProgress
}
private goShortLongStart() {
this.debug(`goShortLongStart -> State.ShortLongInProgress`)
this.resetTimeouts()
this.longPushStart = new Date()
this.sendOutEvent(OutputEvent.ShortLongPressStart)
this.storedSend()
this.state = State.ShortLongInProgress
}
private finalizeShort() {
this.debug(`finalizeShort -> State.Ready`)
this.sendOutEvent(OutputEvent.ShortPress)
this.resetTimeouts()
this.state = State.Ready
}
private finalizeDoubleShort() {
this.debug(`finalizeDoubleShort -> State.Ready`)
this.sendOutEvent(OutputEvent.DoubleShortPress)
this.resetTimeouts()
this.state = State.Ready
}
private finalizeLong() {
this.debug(`finalizeLong -> State.Ready`)
this.sendOutEvent(OutputEvent.LongPressEnd, {
start: this.longPushStart?.getTime(),
duration: (new Date()).getTime() - (this.longPushStart?.getTime() || 0)
})
this.resetTimeouts()
this.state = State.Ready
}
private finalizeShortLong() {
this.debug(`finalizeShortLong -> State.Ready`)
this.sendOutEvent(OutputEvent.ShortLongPressEnd, {
start: this.longPushStart?.getTime(),
duration: (new Date()).getTime() - (this.longPushStart?.getTime() || 0)
})
this.resetTimeouts()
this.state = State.Ready
}
private invalidTransition(event: string) {
this.pConf.warn && this.node.warn(`Invalid transition ${this.state} -> ${event} -> X`)
}
private sendOutEvent(outEvent: OutputEvent, val?: any) {
if (!this.pConf.enabled.short && outEvent === OutputEvent.ShortPress) return
if (!this.pConf.enabled.dblShort && outEvent === OutputEvent.DoubleShortPress) return
if (!this.pConf.enabled.long
&& (outEvent === OutputEvent.LongPressStart || outEvent === OutputEvent.LongPressEnd)
) return
if (!this.pConf.enabled.shortLong
&& (outEvent === OutputEvent.ShortLongPressStart || outEvent === OutputEvent.ShortLongPressEnd)
) return
this.debug(`Sending event ${outEvent} value: ${val}`)
if (this.storedSendFn) {
this.storedSendFn.msg.payload = { event: outEvent }
if (val !== undefined) {
this.storedSendFn.msg.payload.value = val
}
this.storedSendFn.send(this.storedSendFn.msg)
if (this.storedSendFn.done) this.storedSendFn.done()
this.storedSendFn = undefined
}
}
private resetTimeouts() {
this.debug(`resetTimeouts SPTO ${this.shortPushTo}; WAPTO ${this.waitAnotherPushTo}`)
clearTimeout(this.shortPushTo)
clearTimeout(this.waitAnotherPushTo)
this.shortPushTo = undefined
this.waitAnotherPushTo = undefined
this.longPushStart = undefined
}
private storedSend() {
this.storedSendFn = this.latestSendFn
}
private eventWithMsg(msg: any): InputEvent {
const kp = this.pConf.inputKeyPath.split('.')
let sub: any = msg
for (const part of kp) {
if (!sub) return InputEvent.Invalid
sub = sub[part]
}
if (sub === this.pConf.pushValue) return InputEvent.Push
if (sub === this.pConf.releaseValue) return InputEvent.Release
return InputEvent.Invalid
}
private debug(msg: string) {
this.pConf.debug && this.node.debug(msg)
}
}
// Register constructor function with Node-RED
export default function main(RED: NodeAPI) {
RED.nodes.registerType(
'multibutton',
function(this: Node, config: MultiButtonNodeConfig) {
new MultiButton(RED, this, config)
}
)
}