node-red-contrib-garmin-empirbus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/deployment.xml +49 -0
- package/.idea/inspectionProfiles/Project_Default.xml +13 -0
- package/.idea/modules.xml +8 -0
- package/.idea/node-red-contrib-garmin-empirbus.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/package.json +30 -0
- package/scripts/publish.mjs +36 -0
- package/src/helpers/channelHandling.ts +98 -0
- package/src/helpers/getRepository.ts +8 -0
- package/src/nodes/empirbus-config.html +31 -0
- package/src/nodes/empirbus-config.ts +122 -0
- package/src/nodes/empirbus-dim.html +151 -0
- package/src/nodes/empirbus-dim.ts +75 -0
- package/src/nodes/empirbus-switch.html +151 -0
- package/src/nodes/empirbus-switch.ts +99 -0
- package/src/nodes/empirbus-toggle.html +151 -0
- package/src/nodes/empirbus-toggle.ts +65 -0
- package/src/types/EmpirbusConfigNode.ts +14 -0
- package/src/types/EmpirbusToggleAndSwitchNode.ts +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
|
4
|
+
<serverData>
|
|
5
|
+
<paths name="dac7">
|
|
6
|
+
<serverdata>
|
|
7
|
+
<mappings>
|
|
8
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
9
|
+
</mappings>
|
|
10
|
+
</serverdata>
|
|
11
|
+
</paths>
|
|
12
|
+
<paths name="fewomatic">
|
|
13
|
+
<serverdata>
|
|
14
|
+
<mappings>
|
|
15
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
16
|
+
</mappings>
|
|
17
|
+
</serverdata>
|
|
18
|
+
</paths>
|
|
19
|
+
<paths name="server05">
|
|
20
|
+
<serverdata>
|
|
21
|
+
<mappings>
|
|
22
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
23
|
+
</mappings>
|
|
24
|
+
</serverdata>
|
|
25
|
+
</paths>
|
|
26
|
+
<paths name="server05 organizer">
|
|
27
|
+
<serverdata>
|
|
28
|
+
<mappings>
|
|
29
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
30
|
+
</mappings>
|
|
31
|
+
</serverdata>
|
|
32
|
+
</paths>
|
|
33
|
+
<paths name="vac-test">
|
|
34
|
+
<serverdata>
|
|
35
|
+
<mappings>
|
|
36
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
37
|
+
</mappings>
|
|
38
|
+
</serverdata>
|
|
39
|
+
</paths>
|
|
40
|
+
<paths name="vacarema">
|
|
41
|
+
<serverdata>
|
|
42
|
+
<mappings>
|
|
43
|
+
<mapping local="$PROJECT_DIR$" web="/" />
|
|
44
|
+
</mappings>
|
|
45
|
+
</serverdata>
|
|
46
|
+
</paths>
|
|
47
|
+
</serverData>
|
|
48
|
+
</component>
|
|
49
|
+
</project>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<component name="InspectionProjectProfileManager">
|
|
2
|
+
<profile version="1.0">
|
|
3
|
+
<option name="myName" value="Project Default" />
|
|
4
|
+
<inspection_tool class="AiaStyle" enabled="false" level="TYPO" enabled_by_default="false" />
|
|
5
|
+
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
|
6
|
+
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
7
|
+
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
|
8
|
+
<option name="processCode" value="true" />
|
|
9
|
+
<option name="processLiterals" value="true" />
|
|
10
|
+
<option name="processComments" value="true" />
|
|
11
|
+
</inspection_tool>
|
|
12
|
+
</profile>
|
|
13
|
+
</component>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/node-red-contrib-garmin-empirbus.iml" filepath="$PROJECT_DIR$/.idea/node-red-contrib-garmin-empirbus.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
package/.idea/vcs.xml
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-garmin-empirbus",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Connects to Garmin Empirbus MCU or Serv7 Display to enable status and switching",
|
|
6
|
+
"keywords": ["node-red"],
|
|
7
|
+
"node-red": {
|
|
8
|
+
"nodes": {
|
|
9
|
+
"empirbus-config": "dist/nodes/empirbus-config.js",
|
|
10
|
+
"empirbus-dim": "dist/nodes/empirbus-dim.js",
|
|
11
|
+
"empirbus-switch": "dist/nodes/empirbus-switch.js",
|
|
12
|
+
"empirbus-toggle": "dist/nodes/empirbus-toggle.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && yarn copy:html",
|
|
17
|
+
"copy:html": "copyfiles -u 1 \"src/nodes/**/*.html\" dist ",
|
|
18
|
+
"publish:registry": "node scripts/publish.mjs"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"garmin-empirbus-ts": "..\\garmin-empirbus-ts\\garmin-empirbus-ts-v0.1.11.tgz"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.10.0",
|
|
25
|
+
"@types/node-red": "^1.3.5",
|
|
26
|
+
"@types/node-red__editor-api": "^1.3.5",
|
|
27
|
+
"copyfiles": "^2.4.1",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execFile)
|
|
9
|
+
|
|
10
|
+
const registryVersion = '^0.1.11'
|
|
11
|
+
const dependencyName = 'garmin-empirbus-ts'
|
|
12
|
+
|
|
13
|
+
const temp = await mkdtemp(join(tmpdir(), 'npm-publish-'))
|
|
14
|
+
|
|
15
|
+
await exec('npm', ['pack', '--pack-destination', temp], { shell: true })
|
|
16
|
+
|
|
17
|
+
const files = await exec('node', ['-e', `console.log(require("fs").readdirSync(${JSON.stringify(temp)}).join("\\n"))`], { shell: true })
|
|
18
|
+
const tgzName = files.stdout.trim().split('\n').find(name => name.endsWith('.tgz'))
|
|
19
|
+
if (!tgzName) throw new Error('No tgz created by npm pack')
|
|
20
|
+
|
|
21
|
+
const tgzPath = join(temp, tgzName)
|
|
22
|
+
|
|
23
|
+
await exec('tar', ['-xf', tgzPath, '-C', temp], { shell: true })
|
|
24
|
+
|
|
25
|
+
const pkgPath = join(temp, 'package', 'package.json')
|
|
26
|
+
const raw = await readFile(pkgPath, 'utf8')
|
|
27
|
+
const data = JSON.parse(raw)
|
|
28
|
+
|
|
29
|
+
data.dependencies ??= {}
|
|
30
|
+
data.dependencies[dependencyName] = registryVersion
|
|
31
|
+
|
|
32
|
+
await writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
33
|
+
|
|
34
|
+
await exec('npm', ['publish', join(temp, 'package'), '--access', 'public'], { shell: true })
|
|
35
|
+
|
|
36
|
+
await rm(temp, { recursive: true, force: true })
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Channel, EmpirBusChannelRepository } from 'garmin-empirbus-ts'
|
|
2
|
+
import type { NodeMessageInFlow } from 'node-red'
|
|
3
|
+
import { EmpirbusToggleAndSwitchNode } from '../types/EmpirbusToggleAndSwitchNode'
|
|
4
|
+
|
|
5
|
+
export const parseChannelIds = (value?: string): number[] => {
|
|
6
|
+
if (!value)
|
|
7
|
+
return []
|
|
8
|
+
return value
|
|
9
|
+
.split(',')
|
|
10
|
+
.map(s => Number(s.trim()))
|
|
11
|
+
.filter(n => Number.isFinite(n))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const resolveChannelIds = async (node: EmpirbusToggleAndSwitchNode, msg: NodeMessageInFlow, repo: EmpirBusChannelRepository): Promise<number[]> => {
|
|
15
|
+
const fromCheckbox = node.selectedChannelIds ?? []
|
|
16
|
+
if (fromCheckbox.length > 0)
|
|
17
|
+
return fromCheckbox
|
|
18
|
+
|
|
19
|
+
const fromMsgIds = getChannelIdsFromMsg(msg)
|
|
20
|
+
if (fromMsgIds != null)
|
|
21
|
+
return fromMsgIds
|
|
22
|
+
|
|
23
|
+
const fromMsgPayload = getChannelIdsFromTopicOfMsg(msg)
|
|
24
|
+
if (fromMsgPayload != null)
|
|
25
|
+
return fromMsgPayload
|
|
26
|
+
|
|
27
|
+
const fromMsgId = getChannelIdFromMsg(msg)
|
|
28
|
+
if (fromMsgId != null)
|
|
29
|
+
return [fromMsgId]
|
|
30
|
+
|
|
31
|
+
if (typeof node.channelId === 'number' && Number.isFinite(node.channelId))
|
|
32
|
+
return [node.channelId]
|
|
33
|
+
|
|
34
|
+
const fromMsgName = getChannelNameFromMsg(msg)
|
|
35
|
+
const name = fromMsgName ?? node.channelName
|
|
36
|
+
if (!name) return []
|
|
37
|
+
|
|
38
|
+
const index = await ensureChannelIndex(node, repo)
|
|
39
|
+
const mapped = index.get(name)
|
|
40
|
+
if (typeof mapped === 'number' && Number.isFinite(mapped)) {
|
|
41
|
+
return [mapped]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const getChannelIdFromMsg = (msg: NodeMessageInFlow): number | null => {
|
|
48
|
+
const raw = (msg as any).channelId
|
|
49
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
50
|
+
return raw
|
|
51
|
+
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
52
|
+
const n = Number(raw)
|
|
53
|
+
if (Number.isFinite(n))
|
|
54
|
+
return n
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getChannelIdsFromMsg = (msg: NodeMessageInFlow): number[] | null => {
|
|
60
|
+
const raw = (msg as any).channelIds
|
|
61
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
62
|
+
return [raw]
|
|
63
|
+
if (typeof raw === 'string' && raw.trim().length > 0)
|
|
64
|
+
return parseChannelIds(raw)
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const getChannelIdsFromTopicOfMsg = (msg: NodeMessageInFlow): number[] | null => {
|
|
69
|
+
const raw = (msg as any).topic
|
|
70
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
71
|
+
return [raw]
|
|
72
|
+
if (typeof raw === 'string' && raw.trim().length > 0)
|
|
73
|
+
return parseChannelIds(raw)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getChannelNameFromMsg = (msg: NodeMessageInFlow): string | null => {
|
|
78
|
+
const raw = (msg as any).channelName
|
|
79
|
+
if (typeof raw === 'string' && raw.trim().length > 0)
|
|
80
|
+
return raw.trim()
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ensureChannelIndex = async (node: EmpirbusToggleAndSwitchNode, repo: EmpirBusChannelRepository): Promise<Map<string, number>> => {
|
|
85
|
+
|
|
86
|
+
if (node.channelIndexByName)
|
|
87
|
+
return node.channelIndexByName
|
|
88
|
+
|
|
89
|
+
const list = await repo.getChannelList()
|
|
90
|
+
const index = new Map<string, number>()
|
|
91
|
+
list.forEach((ch: Channel) => {
|
|
92
|
+
if (ch.name) {
|
|
93
|
+
index.set(ch.name, ch.id)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
node.channelIndexByName = index
|
|
97
|
+
return index
|
|
98
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EmpirBusChannelRepository } from 'garmin-empirbus-ts'
|
|
2
|
+
import { EmpirbusToggleAndSwitchNode } from '../types/EmpirbusToggleAndSwitchNode'
|
|
3
|
+
|
|
4
|
+
export const getRepository = async (node: EmpirbusToggleAndSwitchNode): Promise<EmpirBusChannelRepository | null> => {
|
|
5
|
+
if (!node.configNode)
|
|
6
|
+
return null
|
|
7
|
+
return node.configNode.getRepository()
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script type="text/x-red" data-template-name="empirbus-config">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-config-input-name">
|
|
4
|
+
<i class="fa fa-tag"></i> Name
|
|
5
|
+
</label>
|
|
6
|
+
<input type="text" id="node-config-input-name">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="form-row">
|
|
10
|
+
<label for="node-config-input-url">
|
|
11
|
+
<i class="fa fa-globe"></i> WebSocket URL
|
|
12
|
+
</label>
|
|
13
|
+
<input type="text" id="node-config-input-url" placeholder="ws://192.168.1.1:8888">
|
|
14
|
+
</div>
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript">
|
|
18
|
+
/* global RED */
|
|
19
|
+
RED.nodes.registerType('empirbus-config', {
|
|
20
|
+
category: 'config',
|
|
21
|
+
defaults: {
|
|
22
|
+
name: { value: '' },
|
|
23
|
+
url: { value: '', required: true }
|
|
24
|
+
},
|
|
25
|
+
label() {
|
|
26
|
+
if (this.name) return this.name
|
|
27
|
+
if (this.url) return this.url
|
|
28
|
+
return 'empirbus-config'
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Channel, EmpirBusChannelRepository, EmpirBusClientState } from 'garmin-empirbus-ts'
|
|
2
|
+
import { NodeDef, NodeInitializer } from 'node-red'
|
|
3
|
+
import { EmpirbusConfigNode, OnStateFn } from '../types/EmpirbusConfigNode'
|
|
4
|
+
|
|
5
|
+
interface EmpirbusConfigNodeDef extends NodeDef {
|
|
6
|
+
name: string
|
|
7
|
+
url: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const nodeInit: NodeInitializer = RED => {
|
|
11
|
+
function scheduleReconnect(node: EmpirbusConfigNode) {
|
|
12
|
+
if (node.timeout) {
|
|
13
|
+
node.log(`node.timeout not null, return`)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
node.timeout = setTimeout(() => {
|
|
17
|
+
if (node.timeout)
|
|
18
|
+
clearTimeout(node.timeout)
|
|
19
|
+
node.repository = connect(node)
|
|
20
|
+
}, 1000)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function connect(node: EmpirbusConfigNode) {
|
|
24
|
+
const repo = new EmpirBusChannelRepository(node.url)
|
|
25
|
+
node.repository = repo
|
|
26
|
+
|
|
27
|
+
node.log(`Connecting to EmpirBus at ${node.url}`)
|
|
28
|
+
|
|
29
|
+
repo.onState(state => {
|
|
30
|
+
if (node.timeout) {
|
|
31
|
+
clearTimeout(node.timeout)
|
|
32
|
+
node.timeout = null
|
|
33
|
+
}
|
|
34
|
+
node.onStateFns.forEach(fn => fn(state))
|
|
35
|
+
switch (state) {
|
|
36
|
+
case EmpirBusClientState.Error:
|
|
37
|
+
node.error(`ERROR connecting to EmpirBus at ${node.url}`)
|
|
38
|
+
scheduleReconnect(node)
|
|
39
|
+
break
|
|
40
|
+
default:
|
|
41
|
+
case EmpirBusClientState.Closed:
|
|
42
|
+
node.log(`Connection to EmpirBus at ${node.url} closed. Trying to reconnect in 1 second.`)
|
|
43
|
+
scheduleReconnect(node)
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
repo
|
|
49
|
+
.connect()
|
|
50
|
+
.then(() => node.log(`Connected to EmpirBus at ${node.url}`))
|
|
51
|
+
.catch(error => {
|
|
52
|
+
node.error(error)
|
|
53
|
+
scheduleReconnect(node)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return repo
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function EmpirbusConfigNodeConstructor(this: EmpirbusConfigNode, config: EmpirbusConfigNodeDef) {
|
|
60
|
+
RED.nodes.createNode(this, config)
|
|
61
|
+
this.name = config.name
|
|
62
|
+
this.url = config.url
|
|
63
|
+
this.repository = null
|
|
64
|
+
this.onStateFns = []
|
|
65
|
+
this.timeout = null
|
|
66
|
+
|
|
67
|
+
const context = this.context()
|
|
68
|
+
context.set('isClosing', false)
|
|
69
|
+
context.set('reconnectTimeout', null)
|
|
70
|
+
|
|
71
|
+
this.repository = connect(this)
|
|
72
|
+
|
|
73
|
+
this.getRepository = async () => {
|
|
74
|
+
if (this.repository)
|
|
75
|
+
return this.repository
|
|
76
|
+
|
|
77
|
+
this.repository = connect(this)
|
|
78
|
+
return this.repository
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.on('close', () => {
|
|
82
|
+
const ctx = this.context()
|
|
83
|
+
ctx.set('isClosing', true)
|
|
84
|
+
|
|
85
|
+
const timeout = ctx.get('reconnectTimeout') as NodeJS.Timeout | null
|
|
86
|
+
if (timeout)
|
|
87
|
+
clearTimeout(timeout)
|
|
88
|
+
|
|
89
|
+
ctx.set('reconnectTimeout', null)
|
|
90
|
+
|
|
91
|
+
const repo = this.repository as unknown as { close?: () => void } | null
|
|
92
|
+
if (repo && typeof repo.close === 'function')
|
|
93
|
+
repo.close()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
this.onState = (fn: OnStateFn) => {
|
|
97
|
+
this.onStateFns.push(fn)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
RED.nodes.registerType('empirbus-config', EmpirbusConfigNodeConstructor as any)
|
|
102
|
+
|
|
103
|
+
RED.httpAdmin.get('/empirbus/:id/channels', async (req, res) => {
|
|
104
|
+
const configNode = RED.nodes.getNode(req.params.id) as EmpirbusConfigNode | null
|
|
105
|
+
if (!configNode) {
|
|
106
|
+
res.status(404).json({ error: 'config not found' })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const repo = await configNode.getRepository()
|
|
111
|
+
repo
|
|
112
|
+
.getChannelList()
|
|
113
|
+
.then((channels: Channel[]) => {
|
|
114
|
+
res.json(channels)
|
|
115
|
+
})
|
|
116
|
+
.catch(error => {
|
|
117
|
+
res.status(500).json({ error: String(error) })
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export = nodeInit
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script type="text/x-red" data-template-name="empirbus-dim">
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
.empirbus-row { display: flex; align-items: flex-start; gap: 8px; }
|
|
5
|
+
.empirbus-row input[type="text"], .empirbus-row input[type="number"] { flex: 1; width: 100%; }
|
|
6
|
+
.empirbus-row label { flex: 0 0 160px; max-width: 160px; white-space: nowrap; padding-top: 7px; }
|
|
7
|
+
.empirbus-input-wrapper { display: flex; flex-direction: column; flex: 1; }
|
|
8
|
+
.empirbus-input-wrapper input { flex: 1; }
|
|
9
|
+
.empirbus-input-wrapper input[type="checkbox"] { flex: 0 0 auto; width: auto; margin-top: 4px; align-self: flex-start; }
|
|
10
|
+
.empirbus-input-wrapper .help { font-size: 0.85em; opacity: 0.8; margin-top: 2px; }
|
|
11
|
+
.empirbus-channel-checkbox { flex: 0 0 20px; }
|
|
12
|
+
.empirbus-channel-id { flex: 0 0 50px; font-weight: bold; opacity: 0.8; text-align: right; display: block; padding-right: 5px; }
|
|
13
|
+
.empirbus-channel-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14
|
+
.empirbus-channel-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
<div class="form-row empirbus-row">
|
|
18
|
+
<label for="node-input-name">
|
|
19
|
+
<i class="fa fa-tag"></i> Name
|
|
20
|
+
</label>
|
|
21
|
+
<input type="text" id="node-input-name">
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="form-row empirbus-row">
|
|
25
|
+
<label for="node-input-config">
|
|
26
|
+
<i class="fa fa-cog"></i> EmpirBus Config
|
|
27
|
+
</label>
|
|
28
|
+
<input type="text" id="node-input-config">
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="form-row empirbus-row">
|
|
32
|
+
<label for="node-input-channelId">
|
|
33
|
+
<i class="fa fa-hashtag"></i> Channel ID
|
|
34
|
+
</label>
|
|
35
|
+
<div class="empirbus-input-wrapper">
|
|
36
|
+
<input type="number" id="node-input-channelId" style="width: 100%;">
|
|
37
|
+
<span class="help">Fallback, wenn keine Checkbox gewählt und kein Name gesetzt ist</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="form-row empirbus-row">
|
|
42
|
+
<label for="node-input-channelName">
|
|
43
|
+
<i class="fa fa-font"></i> Channel Name
|
|
44
|
+
</label>
|
|
45
|
+
<input type="text" id="node-input-channelName">
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="form-row empirbus-row">
|
|
49
|
+
<label for="node-input-acknowledge">
|
|
50
|
+
<i class="fa fa-check"></i> bestätigen
|
|
51
|
+
</label>
|
|
52
|
+
<div class="empirbus-input-wrapper">
|
|
53
|
+
<input type="checkbox" id="node-input-acknowledge">
|
|
54
|
+
<span class="help">Wenn der Befehl erfolgreich ist, wird acknowledge:true an das msg Objekt gehängt</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label>Kanäle</label>
|
|
60
|
+
<div id="empirbus-dim-channels"></div>
|
|
61
|
+
<input type="hidden" id="node-input-channelIds">
|
|
62
|
+
<span class="help">Auswahl mehrerer Kanäle</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script type="text/javascript">
|
|
68
|
+
/* exported oneditprepare, oneditsave */
|
|
69
|
+
/* global RED */
|
|
70
|
+
RED.nodes.registerType('empirbus-dim', {
|
|
71
|
+
category: 'output',
|
|
72
|
+
color: '#a6bbcf',
|
|
73
|
+
defaults: {
|
|
74
|
+
name: { value: '' },
|
|
75
|
+
config: { value: '', type: 'empirbus-config', required: true },
|
|
76
|
+
channelId: { value: '', required: false },
|
|
77
|
+
channelName: { value: '', required: false },
|
|
78
|
+
channelIds: { value: '', required: false },
|
|
79
|
+
acknowledge: { value: false }
|
|
80
|
+
},
|
|
81
|
+
inputs: 1,
|
|
82
|
+
outputs: 1,
|
|
83
|
+
icon: 'bridge.svg',
|
|
84
|
+
label() {
|
|
85
|
+
return this.name || "EmpirBus Dim"
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
oneditprepare() {
|
|
89
|
+
const node = this
|
|
90
|
+
|
|
91
|
+
$('#node-input-acknowledge')
|
|
92
|
+
.prop('checked', !!node.acknowledge)
|
|
93
|
+
|
|
94
|
+
const loadChannels = configId => {
|
|
95
|
+
if (!configId) return
|
|
96
|
+
const container = $('#empirbus-dim-channels')
|
|
97
|
+
container.empty()
|
|
98
|
+
|
|
99
|
+
$.getJSON('empirbus/' + configId + '/channels', channels => {
|
|
100
|
+
const selected = (node.channelIds || '')
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim())
|
|
103
|
+
.filter(s => s.length > 0)
|
|
104
|
+
|
|
105
|
+
channels.forEach(ch => {
|
|
106
|
+
const id = String(ch.id)
|
|
107
|
+
const row = $('<div/>').addClass('empirbus-channel-row')
|
|
108
|
+
|
|
109
|
+
const checkbox = $('<input type="checkbox">')
|
|
110
|
+
.addClass('empirbus-channel-checkbox')
|
|
111
|
+
.attr('data-channel-id', id)
|
|
112
|
+
|
|
113
|
+
if (selected.indexOf(id) !== -1) {
|
|
114
|
+
checkbox.prop('checked', true)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const idLabel = $('<span/>')
|
|
118
|
+
.addClass('empirbus-channel-id')
|
|
119
|
+
.text(id)
|
|
120
|
+
|
|
121
|
+
const labelText = ch.description || ch.name || 'Channel ' + id
|
|
122
|
+
const label = $('<span/>')
|
|
123
|
+
.addClass('empirbus-channel-label')
|
|
124
|
+
.text(labelText)
|
|
125
|
+
|
|
126
|
+
row.append(checkbox).append(idLabel).append(label)
|
|
127
|
+
container.append(row)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$('#node-input-config').on('change', function () {
|
|
133
|
+
loadChannels($(this).val())
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const initialConfig = $('#node-input-config').val()
|
|
137
|
+
if (initialConfig) loadChannels(initialConfig)
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
oneditsave() {
|
|
141
|
+
const ids = []
|
|
142
|
+
$('#empirbus-dim-channels input[type="checkbox"]:checked').each(function () {
|
|
143
|
+
ids.push($(this).attr('data-channel-id'))
|
|
144
|
+
})
|
|
145
|
+
$('#node-input-channelIds').val(ids.join(','))
|
|
146
|
+
|
|
147
|
+
const acknowledge = $('#node-input-acknowledge').is(':checked')
|
|
148
|
+
$('#node-input-acknowledge').val(acknowledge)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DimState } from 'garmin-empirbus-ts'
|
|
2
|
+
import type { NodeDef, NodeInitializer } from 'node-red'
|
|
3
|
+
import { parseChannelIds, resolveChannelIds } from '../helpers/channelHandling'
|
|
4
|
+
import { EmpirbusConfigNode } from '../types/EmpirbusConfigNode'
|
|
5
|
+
import { EmpirbusToggleAndSwitchNode } from '../types/EmpirbusToggleAndSwitchNode'
|
|
6
|
+
import { getRepository } from '../helpers/getRepository'
|
|
7
|
+
|
|
8
|
+
interface EmpirbusDimNodeDef extends NodeDef {
|
|
9
|
+
acknowledge: boolean
|
|
10
|
+
channelId?: string
|
|
11
|
+
channelIds?: string
|
|
12
|
+
channelName?: string
|
|
13
|
+
config: string
|
|
14
|
+
name: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nodeInit: NodeInitializer = RED => {
|
|
18
|
+
function EmpirbusDimNodeConstructor(this: EmpirbusToggleAndSwitchNode, config: EmpirbusDimNodeDef) {
|
|
19
|
+
RED.nodes.createNode(this, config)
|
|
20
|
+
this.acknowledge = config.acknowledge || false
|
|
21
|
+
this.configNode = RED.nodes.getNode(config.config) as EmpirbusConfigNode | null
|
|
22
|
+
this.channelId = config.channelId ? Number(config.channelId) : undefined
|
|
23
|
+
this.channelName = config.channelName || undefined
|
|
24
|
+
this.channelIds = config.channelIds || ''
|
|
25
|
+
this.channelIds = config.channelIds || ''
|
|
26
|
+
this.selectedChannelIds = parseChannelIds(this.channelIds)
|
|
27
|
+
|
|
28
|
+
this.on('input', async msg => {
|
|
29
|
+
const repo = await getRepository(this)
|
|
30
|
+
if (!repo) {
|
|
31
|
+
this.error('No EmpirBus config node configured. Configure and select an EmpirBus config node first!', msg)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ids = await resolveChannelIds(this, msg, repo)
|
|
36
|
+
if (ids.length === 0) {
|
|
37
|
+
this.error('No matching channel found', msg)
|
|
38
|
+
this.send(msg)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
let level = (msg.payload as number) * 10
|
|
44
|
+
if (level < 120 && (msg.payload as number) > 0)
|
|
45
|
+
level = 120
|
|
46
|
+
const results = ids.map(id => repo.dim(id, level as DimState))
|
|
47
|
+
if (results.filter(result => result.hasFailed).length === 0) {
|
|
48
|
+
if (this.acknowledge) {
|
|
49
|
+
msg.acknowledge = true
|
|
50
|
+
msg.payload = {
|
|
51
|
+
state: {
|
|
52
|
+
brightness: msg.payload
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.log(`Dimmed channels ${ids.join(',')} ${msg.payload}, returning message ${JSON.stringify(msg)}`)
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
results.filter(result => result.hasFailed).forEach(result => {
|
|
60
|
+
this.error(result.errors.join(', '), msg)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.send(msg)
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.error(error as Error, msg)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
RED.nodes.registerType('empirbus-dim', EmpirbusDimNodeConstructor)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export = nodeInit
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script type="text/x-red" data-template-name="empirbus-switch">
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
.empirbus-row { display: flex; align-items: flex-start; gap: 8px; }
|
|
5
|
+
.empirbus-row input[type="text"], .empirbus-row input[type="number"] { flex: 1; width: 100%; }
|
|
6
|
+
.empirbus-row label { flex: 0 0 160px; max-width: 160px; white-space: nowrap; padding-top: 7px; }
|
|
7
|
+
.empirbus-input-wrapper { display: flex; flex-direction: column; flex: 1; }
|
|
8
|
+
.empirbus-input-wrapper input { flex: 1; }
|
|
9
|
+
.empirbus-input-wrapper input[type="checkbox"] { flex: 0 0 auto; width: auto; margin-top: 4px; align-self: flex-start; }
|
|
10
|
+
.empirbus-input-wrapper .help { font-size: 0.85em; opacity: 0.8; margin-top: 2px; }
|
|
11
|
+
.empirbus-channel-checkbox { flex: 0 0 20px; }
|
|
12
|
+
.empirbus-channel-id { flex: 0 0 50px; font-weight: bold; opacity: 0.8; text-align: right; display: block; padding-right: 5px; }
|
|
13
|
+
.empirbus-channel-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14
|
+
.empirbus-channel-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
<div class="form-row empirbus-row">
|
|
18
|
+
<label for="node-input-name">
|
|
19
|
+
<i class="fa fa-tag"></i> Name
|
|
20
|
+
</label>
|
|
21
|
+
<input type="text" id="node-input-name">
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="form-row empirbus-row">
|
|
25
|
+
<label for="node-input-config">
|
|
26
|
+
<i class="fa fa-cog"></i> EmpirBus Config
|
|
27
|
+
</label>
|
|
28
|
+
<input type="text" id="node-input-config">
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="form-row empirbus-row">
|
|
32
|
+
<label for="node-input-channelId">
|
|
33
|
+
<i class="fa fa-hashtag"></i> Channel ID
|
|
34
|
+
</label>
|
|
35
|
+
<div class="empirbus-input-wrapper">
|
|
36
|
+
<input type="number" id="node-input-channelId" style="width: 100%;">
|
|
37
|
+
<span class="help">Fallback, wenn keine Checkbox gewählt und kein Name gesetzt ist</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="form-row empirbus-row">
|
|
42
|
+
<label for="node-input-channelName">
|
|
43
|
+
<i class="fa fa-font"></i> Channel Name
|
|
44
|
+
</label>
|
|
45
|
+
<input type="text" id="node-input-channelName">
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="form-row empirbus-row">
|
|
49
|
+
<label for="node-input-acknowledge">
|
|
50
|
+
<i class="fa fa-check"></i> bestätigen
|
|
51
|
+
</label>
|
|
52
|
+
<div class="empirbus-input-wrapper">
|
|
53
|
+
<input type="checkbox" id="node-input-acknowledge">
|
|
54
|
+
<span class="help">Wenn der Befehl erfolgreich ist, wird acknowledge:true an das msg Objekt gehängt</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label>Kanäle</label>
|
|
60
|
+
<div id="empirbus-switch-channels"></div>
|
|
61
|
+
<input type="hidden" id="node-input-channelIds">
|
|
62
|
+
<span class="help">Auswahl mehrerer Kanäle</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script type="text/javascript">
|
|
68
|
+
/* exported oneditprepare, oneditsave */
|
|
69
|
+
/* global RED */
|
|
70
|
+
RED.nodes.registerType('empirbus-switch', {
|
|
71
|
+
category: 'output',
|
|
72
|
+
color: '#a6bbcf',
|
|
73
|
+
defaults: {
|
|
74
|
+
name: { value: '' },
|
|
75
|
+
config: { value: '', type: 'empirbus-config', required: true },
|
|
76
|
+
channelId: { value: '', required: false },
|
|
77
|
+
channelName: { value: '', required: false },
|
|
78
|
+
channelIds: { value: '', required: false },
|
|
79
|
+
acknowledge: { value: false }
|
|
80
|
+
},
|
|
81
|
+
inputs: 1,
|
|
82
|
+
outputs: 1,
|
|
83
|
+
icon: 'bridge.svg',
|
|
84
|
+
label() {
|
|
85
|
+
return this.name || "EmpirBus Switch"
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
oneditprepare() {
|
|
89
|
+
const node = this
|
|
90
|
+
|
|
91
|
+
$('#node-input-acknowledge')
|
|
92
|
+
.prop('checked', !!node.acknowledge)
|
|
93
|
+
|
|
94
|
+
const loadChannels = configId => {
|
|
95
|
+
if (!configId) return
|
|
96
|
+
const container = $('#empirbus-switch-channels')
|
|
97
|
+
container.empty()
|
|
98
|
+
|
|
99
|
+
$.getJSON('empirbus/' + configId + '/channels', channels => {
|
|
100
|
+
const selected = (node.channelIds || '')
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim())
|
|
103
|
+
.filter(s => s.length > 0)
|
|
104
|
+
|
|
105
|
+
channels.forEach(ch => {
|
|
106
|
+
const id = String(ch.id)
|
|
107
|
+
const row = $('<div/>').addClass('empirbus-channel-row')
|
|
108
|
+
|
|
109
|
+
const checkbox = $('<input type="checkbox">')
|
|
110
|
+
.addClass('empirbus-channel-checkbox')
|
|
111
|
+
.attr('data-channel-id', id)
|
|
112
|
+
|
|
113
|
+
if (selected.indexOf(id) !== -1) {
|
|
114
|
+
checkbox.prop('checked', true)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const idLabel = $('<span/>')
|
|
118
|
+
.addClass('empirbus-channel-id')
|
|
119
|
+
.text(id)
|
|
120
|
+
|
|
121
|
+
const labelText = ch.description || ch.name || 'Channel ' + id
|
|
122
|
+
const label = $('<span/>')
|
|
123
|
+
.addClass('empirbus-channel-label')
|
|
124
|
+
.text(labelText)
|
|
125
|
+
|
|
126
|
+
row.append(checkbox).append(idLabel).append(label)
|
|
127
|
+
container.append(row)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$('#node-input-config').on('change', function () {
|
|
133
|
+
loadChannels($(this).val())
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const initialConfig = $('#node-input-config').val()
|
|
137
|
+
if (initialConfig) loadChannels(initialConfig)
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
oneditsave() {
|
|
141
|
+
const ids = []
|
|
142
|
+
$('#empirbus-switch-channels input[type="checkbox"]:checked').each(function () {
|
|
143
|
+
ids.push($(this).attr('data-channel-id'))
|
|
144
|
+
})
|
|
145
|
+
$('#node-input-channelIds').val(ids.join(','))
|
|
146
|
+
|
|
147
|
+
const acknowledge = $('#node-input-acknowledge').is(':checked')
|
|
148
|
+
$('#node-input-acknowledge').val(acknowledge)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
</script>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EmpirBusChannelRepository, EmpirBusClientState } from 'garmin-empirbus-ts'
|
|
2
|
+
import { SwitchState } from 'garmin-empirbus-ts/dist/infrastructure/repositories/EmpirBus/EmpirBusChannelRepository'
|
|
3
|
+
import type { NodeDef, NodeInitializer } from 'node-red'
|
|
4
|
+
import { parseChannelIds, resolveChannelIds } from '../helpers/channelHandling'
|
|
5
|
+
import { EmpirbusConfigNode } from '../types/EmpirbusConfigNode'
|
|
6
|
+
import { EmpirbusToggleAndSwitchNode } from '../types/EmpirbusToggleAndSwitchNode'
|
|
7
|
+
|
|
8
|
+
interface EmpirbusSwitchNodeDef extends NodeDef {
|
|
9
|
+
acknowledge: boolean
|
|
10
|
+
channelId?: string
|
|
11
|
+
channelIds?: string
|
|
12
|
+
channelName?: string
|
|
13
|
+
config: string
|
|
14
|
+
name: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getRepository = async (node: EmpirbusToggleAndSwitchNode): Promise<EmpirBusChannelRepository | null> => {
|
|
18
|
+
if (!node.configNode)
|
|
19
|
+
return null
|
|
20
|
+
return node.configNode.getRepository()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nodeInit: NodeInitializer = RED => {
|
|
24
|
+
function EmpirbusSwitchNodeConstructor(this: EmpirbusToggleAndSwitchNode, config: EmpirbusSwitchNodeDef) {
|
|
25
|
+
RED.nodes.createNode(this, config)
|
|
26
|
+
this.acknowledge = config.acknowledge || false
|
|
27
|
+
this.configNode = RED.nodes.getNode(config.config) as EmpirbusConfigNode | null
|
|
28
|
+
this.channelId = config.channelId ? Number(config.channelId) : undefined
|
|
29
|
+
this.channelName = config.channelName || undefined
|
|
30
|
+
this.channelIds = config.channelIds || ''
|
|
31
|
+
this.channelIds = config.channelIds || ''
|
|
32
|
+
this.selectedChannelIds = parseChannelIds(this.channelIds)
|
|
33
|
+
|
|
34
|
+
if (this.configNode) {
|
|
35
|
+
this.configNode.onState((state: EmpirBusClientState) => {
|
|
36
|
+
switch (state) {
|
|
37
|
+
case EmpirBusClientState.Connected:
|
|
38
|
+
this.status({ fill: 'green', shape: 'dot', text: `connected` })
|
|
39
|
+
break
|
|
40
|
+
case EmpirBusClientState.Error:
|
|
41
|
+
this.status({ fill: 'red', shape: 'dot', text: `ERROR` })
|
|
42
|
+
break
|
|
43
|
+
case EmpirBusClientState.Connecting:
|
|
44
|
+
this.status({ fill: 'red', shape: 'ring', text: `connecting` })
|
|
45
|
+
break
|
|
46
|
+
default:
|
|
47
|
+
case EmpirBusClientState.Closed:
|
|
48
|
+
this.status({ fill: 'red', shape: 'ring', text: `disconnected` })
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.on('input', async msg => {
|
|
55
|
+
const repo = await getRepository(this)
|
|
56
|
+
if (!repo) {
|
|
57
|
+
this.error('No EmpirBus config node configured. Configure and select an EmpirBus config node first!', msg)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ids = await resolveChannelIds(this, msg, repo)
|
|
62
|
+
if (ids.length === 0) {
|
|
63
|
+
this.error('No matching channel found', msg)
|
|
64
|
+
this.send(msg)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const promises = ids.map(id => repo.switch(id, msg.payload as SwitchState))
|
|
70
|
+
const results = await Promise.all(promises)
|
|
71
|
+
if (results.filter(result => result.hasFailed).length === 0) {
|
|
72
|
+
if (this.acknowledge) {
|
|
73
|
+
msg.acknowledge = true
|
|
74
|
+
msg.payload = {
|
|
75
|
+
state: {
|
|
76
|
+
power: msg.payload
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.log(`Switched channels ${ids.join(',')} ${msg.payload}, returning message ${JSON.stringify(msg)}`)
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
results.filter(result => result.hasFailed).forEach(result => {
|
|
84
|
+
this.error(result.errors.join(', '), msg)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.send(msg)
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.error(error as Error, msg)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
RED.nodes.registerType('empirbus-switch', EmpirbusSwitchNodeConstructor)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export = nodeInit
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script type="text/x-red" data-template-name="empirbus-toggle">
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
.empirbus-row { display: flex; align-items: flex-start; gap: 8px; }
|
|
5
|
+
.empirbus-row input[type="text"], .empirbus-row input[type="number"] { flex: 1; width: 100%; }
|
|
6
|
+
.empirbus-row label { flex: 0 0 160px; max-width: 160px; white-space: nowrap; padding-top: 7px; }
|
|
7
|
+
.empirbus-input-wrapper { display: flex; flex-direction: column; flex: 1; }
|
|
8
|
+
.empirbus-input-wrapper input { flex: 1; }
|
|
9
|
+
.empirbus-input-wrapper input[type="checkbox"] { flex: 0 0 auto; width: auto; margin-top: 4px; align-self: flex-start; }
|
|
10
|
+
.empirbus-input-wrapper .help { font-size: 0.85em; opacity: 0.8; margin-top: 2px; }
|
|
11
|
+
.empirbus-channel-checkbox { flex: 0 0 20px; }
|
|
12
|
+
.empirbus-channel-id { flex: 0 0 50px; font-weight: bold; opacity: 0.8; text-align: right; display: block; padding-right: 5px; }
|
|
13
|
+
.empirbus-channel-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14
|
+
.empirbus-channel-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
<div class="form-row empirbus-row">
|
|
18
|
+
<label for="node-input-name">
|
|
19
|
+
<i class="fa fa-tag"></i> Name
|
|
20
|
+
</label>
|
|
21
|
+
<input type="text" id="node-input-name">
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="form-row empirbus-row">
|
|
25
|
+
<label for="node-input-config">
|
|
26
|
+
<i class="fa fa-cog"></i> EmpirBus Config
|
|
27
|
+
</label>
|
|
28
|
+
<input type="text" id="node-input-config">
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="form-row empirbus-row">
|
|
32
|
+
<label for="node-input-channelId">
|
|
33
|
+
<i class="fa fa-hashtag"></i> Channel ID
|
|
34
|
+
</label>
|
|
35
|
+
<div class="empirbus-input-wrapper">
|
|
36
|
+
<input type="number" id="node-input-channelId" style="width: 100%;">
|
|
37
|
+
<span class="help">Fallback, wenn keine Checkbox gewählt und kein Name gesetzt ist</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="form-row empirbus-row">
|
|
42
|
+
<label for="node-input-channelName">
|
|
43
|
+
<i class="fa fa-font"></i> Channel Name
|
|
44
|
+
</label>
|
|
45
|
+
<input type="text" id="node-input-channelName">
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="form-row empirbus-row">
|
|
49
|
+
<label for="node-input-acknowledge">
|
|
50
|
+
<i class="fa fa-check"></i> bestätigen
|
|
51
|
+
</label>
|
|
52
|
+
<div class="empirbus-input-wrapper">
|
|
53
|
+
<input type="checkbox" id="node-input-acknowledge">
|
|
54
|
+
<span class="help">Wenn der Befehl erfolgreich ist, wird acknowledge:true an das msg Objekt gehängt</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label>Kanäle</label>
|
|
60
|
+
<div id="empirbus-toggle-channels"></div>
|
|
61
|
+
<input type="hidden" id="node-input-channelIds">
|
|
62
|
+
<span class="help">Auswahl mehrerer Kanäle</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script type="text/javascript">
|
|
68
|
+
/* exported oneditprepare, oneditsave */
|
|
69
|
+
/* global RED */
|
|
70
|
+
RED.nodes.registerType('empirbus-toggle', {
|
|
71
|
+
category: 'output',
|
|
72
|
+
color: '#a6bbcf',
|
|
73
|
+
defaults: {
|
|
74
|
+
name: { value: '' },
|
|
75
|
+
config: { value: '', type: 'empirbus-config', required: true },
|
|
76
|
+
channelId: { value: '', required: false },
|
|
77
|
+
channelName: { value: '', required: false },
|
|
78
|
+
channelIds: { value: '', required: false },
|
|
79
|
+
acknowledge: { value: false }
|
|
80
|
+
},
|
|
81
|
+
inputs: 1,
|
|
82
|
+
outputs: 1,
|
|
83
|
+
icon: 'bridge.svg',
|
|
84
|
+
label() {
|
|
85
|
+
return this.name || "EmpirBus Toggle"
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
oneditprepare() {
|
|
89
|
+
const node = this
|
|
90
|
+
|
|
91
|
+
$('#node-input-acknowledge')
|
|
92
|
+
.prop('checked', !!node.acknowledge)
|
|
93
|
+
|
|
94
|
+
const loadChannels = configId => {
|
|
95
|
+
if (!configId) return
|
|
96
|
+
const container = $('#empirbus-toggle-channels')
|
|
97
|
+
container.empty()
|
|
98
|
+
|
|
99
|
+
$.getJSON('empirbus/' + configId + '/channels', channels => {
|
|
100
|
+
const selected = (node.channelIds || '')
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim())
|
|
103
|
+
.filter(s => s.length > 0)
|
|
104
|
+
|
|
105
|
+
channels.forEach(ch => {
|
|
106
|
+
const id = String(ch.id)
|
|
107
|
+
const row = $('<div/>').addClass('empirbus-channel-row')
|
|
108
|
+
|
|
109
|
+
const checkbox = $('<input type="checkbox">')
|
|
110
|
+
.addClass('empirbus-channel-checkbox')
|
|
111
|
+
.attr('data-channel-id', id)
|
|
112
|
+
|
|
113
|
+
if (selected.indexOf(id) !== -1) {
|
|
114
|
+
checkbox.prop('checked', true)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const idLabel = $('<span/>')
|
|
118
|
+
.addClass('empirbus-channel-id')
|
|
119
|
+
.text(id)
|
|
120
|
+
|
|
121
|
+
const labelText = ch.description || ch.name || 'Channel ' + id
|
|
122
|
+
const label = $('<span/>')
|
|
123
|
+
.addClass('empirbus-channel-label')
|
|
124
|
+
.text(labelText)
|
|
125
|
+
|
|
126
|
+
row.append(checkbox).append(idLabel).append(label)
|
|
127
|
+
container.append(row)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$('#node-input-config').on('change', function () {
|
|
133
|
+
loadChannels($(this).val())
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const initialConfig = $('#node-input-config').val()
|
|
137
|
+
if (initialConfig) loadChannels(initialConfig)
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
oneditsave() {
|
|
141
|
+
const ids = []
|
|
142
|
+
$('#empirbus-toggle-channels input[type="checkbox"]:checked').each(function () {
|
|
143
|
+
ids.push($(this).attr('data-channel-id'))
|
|
144
|
+
})
|
|
145
|
+
$('#node-input-channelIds').val(ids.join(','))
|
|
146
|
+
|
|
147
|
+
const acknowledge = $('#node-input-acknowledge').is(':checked')
|
|
148
|
+
$('#node-input-acknowledge').val(acknowledge)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
</script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EmpirBusChannelRepository } from 'garmin-empirbus-ts'
|
|
2
|
+
import { SwitchState } from 'garmin-empirbus-ts/dist/infrastructure/repositories/EmpirBus/EmpirBusChannelRepository'
|
|
3
|
+
import type { Node as NodeRedNode, NodeDef, NodeInitializer } from 'node-red'
|
|
4
|
+
import { parseChannelIds, resolveChannelIds } from '../helpers/channelHandling'
|
|
5
|
+
import { EmpirbusConfigNode } from '../types/EmpirbusConfigNode'
|
|
6
|
+
import { EmpirbusToggleAndSwitchNode } from '../types/EmpirbusToggleAndSwitchNode'
|
|
7
|
+
|
|
8
|
+
interface EmpirbusToggleNodeDef extends NodeDef {
|
|
9
|
+
acknowledge: boolean
|
|
10
|
+
channelId?: string
|
|
11
|
+
channelIds?: string
|
|
12
|
+
channelName?: string
|
|
13
|
+
config: string
|
|
14
|
+
name: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getRepository = async (node: EmpirbusToggleAndSwitchNode): Promise<EmpirBusChannelRepository | null> => {
|
|
18
|
+
if (!node.configNode)
|
|
19
|
+
return null
|
|
20
|
+
return node.configNode.getRepository()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nodeInit: NodeInitializer = RED => {
|
|
24
|
+
function EmpirbusToggleNodeConstructor(this: EmpirbusToggleAndSwitchNode, config: EmpirbusToggleNodeDef) {
|
|
25
|
+
RED.nodes.createNode(this, config)
|
|
26
|
+
this.acknowledge = config.acknowledge || false
|
|
27
|
+
this.configNode = RED.nodes.getNode(config.config) as EmpirbusConfigNode | null
|
|
28
|
+
this.channelId = config.channelId ? Number(config.channelId) : undefined
|
|
29
|
+
this.channelName = config.channelName || undefined
|
|
30
|
+
this.channelIds = config.channelIds || ''
|
|
31
|
+
this.channelIds = config.channelIds || ''
|
|
32
|
+
this.selectedChannelIds = parseChannelIds(this.channelIds)
|
|
33
|
+
|
|
34
|
+
this.on('input', async msg => {
|
|
35
|
+
const repo = await getRepository(this)
|
|
36
|
+
if (!repo) {
|
|
37
|
+
this.error('No EmpirBus config node configured. Configure and select an EmpirBus config node first!', msg)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ids = await resolveChannelIds(this, msg, repo)
|
|
42
|
+
if (ids.length === 0) {
|
|
43
|
+
this.error('No matching channel found', msg)
|
|
44
|
+
this.send(msg)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const promises = ids.map(id => repo.toggle(id))
|
|
50
|
+
await Promise.all(promises)
|
|
51
|
+
if (this.acknowledge)
|
|
52
|
+
msg.acknowledge = true
|
|
53
|
+
this.log(`Toggled channels ${ids.join(',')}, returning message ${JSON.stringify(msg)}`)
|
|
54
|
+
this.send(msg)
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.error(error as Error, msg)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
RED.nodes.registerType('empirbus-toggle', EmpirbusToggleNodeConstructor)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export = nodeInit
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EmpirBusChannelRepository, EmpirBusClientState } from 'garmin-empirbus-ts'
|
|
2
|
+
import { Node as NodeRed } from 'node-red'
|
|
3
|
+
|
|
4
|
+
export interface EmpirbusConfigNode extends NodeRed {
|
|
5
|
+
name: string
|
|
6
|
+
url: string
|
|
7
|
+
repository: EmpirBusChannelRepository | null
|
|
8
|
+
getRepository: () => Promise<EmpirBusChannelRepository>
|
|
9
|
+
onState: (fn: OnStateFn) => void
|
|
10
|
+
onStateFns: Array<OnStateFn>
|
|
11
|
+
timeout: NodeJS.Timeout | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type OnStateFn = (state: EmpirBusClientState) => void
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Node as NodeRedNode } from 'node-red'
|
|
2
|
+
import { EmpirbusConfigNode } from './EmpirbusConfigNode'
|
|
3
|
+
|
|
4
|
+
export interface EmpirbusToggleAndSwitchNode extends NodeRedNode {
|
|
5
|
+
acknowledge: boolean
|
|
6
|
+
channelId?: number
|
|
7
|
+
channelIds?: string
|
|
8
|
+
channelIndexByName?: Map<string, number>
|
|
9
|
+
channelName?: string
|
|
10
|
+
configNode: EmpirbusConfigNode | null
|
|
11
|
+
selectedChannelIds?: number[]
|
|
12
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"esModuleInterop": true,
|
|
4
|
+
"forceConsistentCasingInFileNames": true,
|
|
5
|
+
"module": "CommonJS",
|
|
6
|
+
"moduleResolution": "Node",
|
|
7
|
+
"noEmitOnError": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"target": "ES2021"
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src"
|
|
18
|
+
]
|
|
19
|
+
}
|