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.
@@ -0,0 +1,5 @@
1
+ <component name="ProjectCodeStyleConfiguration">
2
+ <state>
3
+ <option name="PREFERRED_PROJECT_CODE_STYLE" value="xsigns" />
4
+ </state>
5
+ </component>
@@ -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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
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
+ }