node-red-contrib-esphome 0.0.1

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,118 @@
1
+ import {NodeAPI} from 'node-red';
2
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
3
+ const {Client} = require('esphome-native-api');
4
+
5
+ module.exports = (RED: NodeAPI) => {
6
+ RED.nodes.registerType(
7
+ 'esphome-device',
8
+ function (this: any, config: any) {
9
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
10
+ const self = this;
11
+ self.config = config;
12
+
13
+ RED.nodes.createNode(this, config);
14
+
15
+ self.device = {};
16
+ self.entities = [];
17
+ self.current_status = 'disconnected';
18
+
19
+ if (!config?.host || !config?.port || !self.credentials?.password) {
20
+ return;
21
+ }
22
+
23
+ self.onStatus = function (string: string) {
24
+ self.current_status = string;
25
+ self.emit('onStatus', string);
26
+ };
27
+
28
+ self.onState = function (object: any) {
29
+ self.emit('onState', object);
30
+ };
31
+
32
+ self.client = new Client({
33
+ host: config.host,
34
+ port: config.port,
35
+ password: self.credentials.password,
36
+ clientInfo: 'node-red',
37
+ initializeDeviceInfo: true,
38
+ initializeListEntities: true,
39
+ initializeSubscribeStates: false,
40
+ reconnect: true,
41
+ reconnectInterval: 15 * 1000,
42
+ pingInterval: 5 * 1000
43
+ });
44
+
45
+ try {
46
+ self.client.connect();
47
+ } catch (e: any) {
48
+ self.error(e.message);
49
+ return;
50
+ }
51
+
52
+ self.client.on('error', (e: Error) => {
53
+ if (e.message.includes('EHOSTUNREACH')) {
54
+ /* empty */
55
+ } else if (e.message.includes('Invalid password')) {
56
+ self.error(e.message);
57
+ } else if (e.message.includes('ECONNRESET')) {
58
+ /* empty */
59
+ } else if (e.message.includes('timeout')) {
60
+ /* empty */
61
+ } else if (e.message.includes('write after end')) {
62
+ /* empty */
63
+ } else {
64
+ // copy this error to issues ...
65
+ }
66
+ self.onStatus('error');
67
+ });
68
+
69
+ self.client.on('disconnected', () => {
70
+ self.onStatus('disconnected');
71
+ });
72
+
73
+ self.client.on('connected', () => {
74
+ // clear entities
75
+ self.entities = [];
76
+
77
+ self.onStatus('connecting');
78
+ });
79
+
80
+ self.client.on('initialized', () => {
81
+ self.onStatus('connected');
82
+ });
83
+
84
+ self.client.on('deviceInfo', (deviceInfo: any) => {
85
+ self.device = deviceInfo;
86
+ });
87
+
88
+ self.client.on('newEntity', (entity: any) => {
89
+ self.entities.push({
90
+ key: entity.id,
91
+ type: entity.type,
92
+ name: entity.name,
93
+ config: entity.config
94
+ });
95
+
96
+ entity.connection.subscribeStatesService();
97
+
98
+ entity.on('state', (state: any) => {
99
+ self.onState({...state});
100
+ });
101
+
102
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
+ entity.on('error', (e: Error) => {
104
+ /* empty */
105
+ });
106
+ });
107
+
108
+ self.on('close', () => {
109
+ self.client.disconnect();
110
+ });
111
+ },
112
+ {
113
+ credentials: {
114
+ password: {type: 'password'}
115
+ }
116
+ }
117
+ );
118
+ };
@@ -0,0 +1,83 @@
1
+ import {NodeAPI} from 'node-red';
2
+ import {Status} from '../lib/utils';
3
+ import {inspect} from 'util';
4
+
5
+ module.exports = (RED: NodeAPI) => {
6
+ RED.nodes.registerType('esphome-in', function (this: any, config: any) {
7
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
8
+ const self = this;
9
+ self.config = config;
10
+
11
+ RED.nodes.createNode(this, config);
12
+
13
+ try {
14
+ self.deviceNode = RED.nodes.getNode(self.config.device);
15
+ } catch (_) {
16
+ /* empty */
17
+ }
18
+
19
+ if (!self.deviceNode || !self.config.entity) {
20
+ return;
21
+ }
22
+
23
+ self.current_status = self.deviceNode.current_status;
24
+
25
+ const clearStatus = (timeout = 0) => {
26
+ setTimeout(() => {
27
+ if (self.current_status) {
28
+ self.status(Status[self.current_status as string]);
29
+ } else {
30
+ self.status({});
31
+ }
32
+ }, timeout);
33
+ };
34
+
35
+ const setStatus = (status: any, timeout = 0) => {
36
+ self.status(status);
37
+ if (timeout) {
38
+ clearStatus(timeout);
39
+ }
40
+ };
41
+
42
+ const onState = (state: any) => {
43
+ if (state.key != config.entity) {
44
+ return;
45
+ }
46
+
47
+ delete state.key;
48
+
49
+ let data = typeof state.state !== 'undefined' && typeof state.state !== 'object' ? state.state : inspect(state);
50
+ if (data && data.length > 32) {
51
+ data = data.substr(0, 32) + '...';
52
+ }
53
+ setStatus({fill: 'yellow', shape: 'dot', text: data}, 3000);
54
+
55
+ const entity: any = self.deviceNode.entities.find((e: any) => e.key == config.entity);
56
+
57
+ self.send({
58
+ payload: state,
59
+ device: self.deviceNode.device,
60
+ config: entity?.config,
61
+ entity: entity
62
+ });
63
+ };
64
+
65
+ const onStatus = (status: string) => {
66
+ self.current_status = status;
67
+
68
+ setStatus(Status[self.current_status as string]);
69
+ };
70
+
71
+ self.onState = (state: any) => onState(state);
72
+ self.deviceNode.on('onState', self.onState);
73
+
74
+ self.onStatus = (status: string) => onStatus(status);
75
+ self.deviceNode.on('onStatus', self.onStatus);
76
+
77
+ self.on('close', (_: any, done: () => any) => {
78
+ self.deviceNode.removeListener('onState', self.onState);
79
+ self.deviceNode.removeListener('onStatus', self.onStatus);
80
+ done();
81
+ });
82
+ });
83
+ };
@@ -0,0 +1,83 @@
1
+ import {NodeAPI} from 'node-red';
2
+ import {Status} from '../lib/utils';
3
+ import {inspect} from 'util';
4
+
5
+ module.exports = (RED: NodeAPI) => {
6
+ RED.nodes.registerType('esphome-out', function (this: any, config: any) {
7
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
8
+ const self = this;
9
+ self.config = config;
10
+
11
+ RED.nodes.createNode(this, config);
12
+
13
+ try {
14
+ self.deviceNode = RED.nodes.getNode(self.config.device);
15
+ } catch (_) {
16
+ /* empty */
17
+ }
18
+
19
+ if (!self.deviceNode || !self.config.entity) {
20
+ return;
21
+ }
22
+
23
+ self.current_status = self.deviceNode.current_status;
24
+
25
+ const clearStatus = (timeout = 0) => {
26
+ setTimeout(() => {
27
+ if (self.current_status) {
28
+ self.status(Status[self.current_status as string]);
29
+ } else {
30
+ self.status({});
31
+ }
32
+ }, timeout);
33
+ };
34
+
35
+ const setStatus = (status: any, timeout = 0) => {
36
+ self.status(status);
37
+ if (timeout) {
38
+ clearStatus(timeout);
39
+ }
40
+ };
41
+
42
+ self.on('input', async (msg: any, send: () => any, done: () => any) => {
43
+ const entity: any = self.deviceNode.entities.find((e: any) => e.key == config.entity);
44
+ const command = entity.type.toLowerCase() + 'CommandService';
45
+
46
+ const regexp = /^(BinarySensor|Sensor|TextSensor)$/gi;
47
+ if (entity.type.match(regexp)) {
48
+ done();
49
+ return;
50
+ }
51
+
52
+ let data = msg.payload;
53
+ data = typeof data.state !== 'undefined' && typeof data.state !== 'object' ? data.state : inspect(data);
54
+ if (data && data.length > 32) {
55
+ data = data.substr(0, 32) + '...';
56
+ }
57
+ setStatus({fill: 'yellow', shape: 'dot', text: data}, 3000);
58
+
59
+ try {
60
+ await self.deviceNode.client.connection[command]({key: config.entity, ...msg.payload});
61
+ } catch (e: any) {
62
+ setStatus(Status['error'], 3000);
63
+ self.error(e.message);
64
+ }
65
+
66
+ done();
67
+ });
68
+
69
+ const onStatus = (status: string) => {
70
+ self.current_status = status;
71
+
72
+ setStatus(Status[self.current_status as string]);
73
+ };
74
+
75
+ self.onStatus = (status: string) => onStatus(status);
76
+ self.deviceNode.on('onStatus', self.onStatus);
77
+
78
+ self.on('close', (_: any, done: () => any) => {
79
+ self.deviceNode.removeListener('onStatus', self.onStatus);
80
+ done();
81
+ });
82
+ });
83
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "esModuleInterop": true,
5
+ "module": "commonjs",
6
+ "moduleResolution": "node",
7
+ "outDir": "build",
8
+ "rootDir": "src",
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "target": "es6"
12
+ },
13
+ "include": [
14
+ "src"
15
+ ],
16
+ "exclude": [
17
+ "node_modules",
18
+ "build"
19
+ ]
20
+ }