@haydendonald/node-red-contrib-hass-stuff 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,311 @@
1
+ "use strict";
2
+ const lightControlTypes_1 = require("./lightControlTypes");
3
+ const baseConfigNode_1 = require("../baseConfigNode");
4
+ const utility_1 = require("../utility");
5
+ module.exports = function LightControlConfigNode(RED) {
6
+ const register = function (config) {
7
+ const self = this;
8
+ let groupEntities = [];
9
+ let currentState;
10
+ let nightModeState;
11
+ let currentSceneState;
12
+ let sunState;
13
+ let entitiesOnDuringNight = [];
14
+ let entitiesOffDuringNight = [];
15
+ let adaptiveInterval;
16
+ let errored = false;
17
+ if (!config.groupEntityId) {
18
+ this.error("Group Entity ID is required");
19
+ errored = true;
20
+ }
21
+ if (!config.concentrateSettings) {
22
+ this.error("Concentrate Settings is required");
23
+ errored = true;
24
+ }
25
+ if (!config.readSettings) {
26
+ this.error("Read Settings is required");
27
+ errored = true;
28
+ }
29
+ if (!config.relaxSettings) {
30
+ this.error("Relax Settings is required");
31
+ errored = true;
32
+ }
33
+ if (!config.restSettings) {
34
+ this.error("Rest Settings is required");
35
+ errored = true;
36
+ }
37
+ if (!config.nightLightSettings) {
38
+ this.error("Night Light Settings is required");
39
+ errored = true;
40
+ }
41
+ if (!config.nightSettings) {
42
+ this.error("Night Settings is required");
43
+ errored = true;
44
+ }
45
+ //Check that all the scenes are following [scene] [brightnessPercent]
46
+ const sceneRegex = /^[^\s]+ \d+$/;
47
+ if (!sceneRegex.test(config.concentrateSettings)) {
48
+ this.error("Concentrate Settings must be in the format [scene] [brightnessPercent]");
49
+ errored = true;
50
+ }
51
+ if (!sceneRegex.test(config.readSettings)) {
52
+ this.error("Read Settings must be in the format [scene] [brightnessPercent]");
53
+ errored = true;
54
+ }
55
+ if (!sceneRegex.test(config.relaxSettings)) {
56
+ this.error("Relax Settings must be in the format [scene] [brightnessPercent]");
57
+ errored = true;
58
+ }
59
+ if (!sceneRegex.test(config.restSettings)) {
60
+ this.error("Rest Settings must be in the format [scene] [brightnessPercent]");
61
+ errored = true;
62
+ }
63
+ if (!sceneRegex.test(config.nightLightSettings)) {
64
+ this.error("Night Light Settings must be in the format [scene] [brightnessPercent]");
65
+ errored = true;
66
+ }
67
+ if (!sceneRegex.test(config.nightSettings)) {
68
+ this.error("Night Settings must be in the format [scene] [brightnessPercent]");
69
+ errored = true;
70
+ }
71
+ if (errored) {
72
+ return;
73
+ }
74
+ const scenes = {
75
+ concentrate: {
76
+ friendlyName: "Concentrate",
77
+ sceneName: config.concentrateSettings.split(" ")[0],
78
+ brightnessPct: parseInt(config.concentrateSettings.split(" ")[1])
79
+ },
80
+ read: {
81
+ friendlyName: "Read",
82
+ sceneName: config.readSettings.split(" ")[0],
83
+ brightnessPct: parseInt(config.readSettings.split(" ")[1])
84
+ },
85
+ relax: {
86
+ friendlyName: "Relax",
87
+ sceneName: config.relaxSettings.split(" ")[0],
88
+ brightnessPct: parseInt(config.relaxSettings.split(" ")[1])
89
+ },
90
+ nightLight: {
91
+ friendlyName: "Night Light",
92
+ sceneName: config.nightLightSettings.split(" ")[0],
93
+ brightnessPct: parseInt(config.nightLightSettings.split(" ")[1])
94
+ },
95
+ night: {
96
+ friendlyName: "Night",
97
+ sceneName: config.nightSettings.split(" ")[0],
98
+ brightnessPct: parseInt(config.nightSettings.split(" ")[1])
99
+ }
100
+ };
101
+ const getSceneFromFriendlyName = (friendlyName) => {
102
+ return Object.values(scenes).find(scene => scene.friendlyName === friendlyName);
103
+ };
104
+ RED.nodes.createNode(this, config);
105
+ (0, baseConfigNode_1.assignBaseConfigNode)(this);
106
+ const connectionsConfigNode = RED.nodes.getNode(config.connectionsConfigNode);
107
+ //When HASS is ready
108
+ connectionsConfigNode.hassEventReadyCallbacks[this.id] = function (msg) {
109
+ //Get what entities exist in the group and set our internal state
110
+ connectionsConfigNode.getHASSEntityState(config.groupEntityId, (payload, data) => {
111
+ groupEntities = data.data.attributes.entity_id;
112
+ currentState = data.data.state;
113
+ entitiesOffDuringNight = config.entitiesOffAtNight.split(",");
114
+ entitiesOnDuringNight = groupEntities.filter((entity) => !entitiesOffDuringNight.includes(entity));
115
+ });
116
+ //Get the state of the night mode switch and store it
117
+ if (config.nightModeEntityId && config.nightModeEntityId != "") {
118
+ connectionsConfigNode.getHASSEntityState(config.nightModeEntityId, (payload, data) => {
119
+ nightModeState = data.data.state;
120
+ });
121
+ }
122
+ //Get the state of the sun
123
+ connectionsConfigNode.getHASSEntityState("sun.sun", (payload, data) => {
124
+ sunState = data.data.state;
125
+ });
126
+ //Add our scenes to home assistant
127
+ for (const [sceneKey, sceneValue] of Object.entries(scenes)) {
128
+ connectionsConfigNode.addHASSScene({
129
+ friendlyName: `${self.name} - ${sceneValue.friendlyName}`,
130
+ id: (0, utility_1.getEntityId)("scene", `${self.name}_${sceneKey}`),
131
+ creationCallback: (response) => { },
132
+ activatedCallback: (serviceData) => {
133
+ //Set our scene
134
+ connectionsConfigNode.sendHASSAction("select.select_option", { entity_id: [(0, utility_1.getEntityId)("select", `${self.name}_current_scene`)] }, {
135
+ option: sceneValue.friendlyName
136
+ });
137
+ activateScene(sceneValue, serviceData.transition || 1, true);
138
+ }
139
+ });
140
+ }
141
+ //Add our adaptive scene
142
+ connectionsConfigNode.addHASSScene({
143
+ friendlyName: `${self.name} - Adaptive`,
144
+ id: (0, utility_1.getEntityId)("scene", `${self.name}_adaptive`),
145
+ creationCallback: (response) => { },
146
+ activatedCallback: (serviceData) => {
147
+ //Set our scene to adaptive
148
+ connectionsConfigNode.sendHASSAction("select.select_option", { entity_id: [(0, utility_1.getEntityId)("select", `${self.name}_current_scene`)] }, {
149
+ option: "Adaptive"
150
+ });
151
+ runAdaptive(serviceData.transition || 1, true);
152
+ }
153
+ });
154
+ //Add our turn on scene
155
+ connectionsConfigNode.addHASSScene({
156
+ friendlyName: `${self.name} - Turn On`,
157
+ id: (0, utility_1.getEntityId)("scene", `${self.name}_turn_on`),
158
+ creationCallback: (response) => { },
159
+ activatedCallback: (serviceData) => {
160
+ runLights(serviceData.transition || 1, true);
161
+ }
162
+ });
163
+ //Add our turn off scene
164
+ connectionsConfigNode.addHASSScene({
165
+ friendlyName: `${self.name} - Turn Off`,
166
+ id: (0, utility_1.getEntityId)("scene", `${self.name}_turn_off`),
167
+ creationCallback: (response) => { },
168
+ activatedCallback: (serviceData) => {
169
+ connectionsConfigNode.sendHASSAction("light.turn_off", { entity_id: [config.groupEntityId] }, {
170
+ transition: serviceData.transition || 1
171
+ });
172
+ }
173
+ });
174
+ //Add our select to keep track of what scene we are running
175
+ connectionsConfigNode.addHASSSelect({
176
+ friendlyName: `${self.name} - Current Scene`,
177
+ id: (0, utility_1.getEntityId)("select", `${self.name}_current_scene`),
178
+ options: [
179
+ ...Object.values(scenes).map(scene => scene.friendlyName),
180
+ "Adaptive"
181
+ ],
182
+ creationCallback: (state) => {
183
+ currentSceneState = state;
184
+ runLights(300, false);
185
+ },
186
+ activatedCallback: (state, serviceData) => {
187
+ currentSceneState = state;
188
+ }
189
+ });
190
+ };
191
+ //When a state change happens in home assistant
192
+ connectionsConfigNode.hassEventStateChangeCallbacks[this.id] = function (entityId, oldState, newState) {
193
+ switch (entityId) {
194
+ case config.groupEntityId: {
195
+ currentState = newState;
196
+ break;
197
+ }
198
+ case config.nightModeEntityId: {
199
+ nightModeState = newState.state;
200
+ break;
201
+ }
202
+ case "sun.sun": {
203
+ sunState = newState.state;
204
+ break;
205
+ }
206
+ }
207
+ };
208
+ //When we get a message from a node on the NodeRED flows
209
+ this.msgReceived = function (msg, senderIds) { };
210
+ const activateScene = (scene, transitionSec, turnLightsOn, entitiesOn, entitiesOff) => {
211
+ //If the lights are off and we are not to turn the lights on don't do anything
212
+ if (turnLightsOn == false && currentState == "off") {
213
+ return;
214
+ }
215
+ //Is not a scene preset scene if we have a entity id
216
+ if (scene.sceneName.includes(".")) {
217
+ //Run via hass scene.turn_on
218
+ connectionsConfigNode.sendHASSAction("scene.turn_on", {
219
+ entity_id: [scene.sceneName]
220
+ }, {
221
+ transition: transitionSec
222
+ });
223
+ }
224
+ //Is a scene preset
225
+ else {
226
+ const sceneId = lightControlTypes_1.ScenePresets[scene.sceneName];
227
+ if (!sceneId) {
228
+ self.error(`Scene preset not found for ${scene.sceneName}. Please use scene.<presetname> if this is not a scene i know about`);
229
+ return;
230
+ }
231
+ //Run via adaptive lights
232
+ connectionsConfigNode.sendHASSAction("scene_presets.apply_preset", undefined, {
233
+ brightness: (scene.brightnessPct / 100.0) * 255,
234
+ transition: transitionSec,
235
+ preset_id: sceneId,
236
+ shuffle: true,
237
+ smart_shuffle: true,
238
+ targets: {
239
+ entity_id: entitiesOn ? entitiesOn : [config.groupEntityId]
240
+ }
241
+ }, undefined);
242
+ //Run any lights off that need to be off
243
+ if (entitiesOff && entitiesOff.length > 0) {
244
+ connectionsConfigNode.sendHASSAction("light.turn_off", {
245
+ entity_id: entitiesOff
246
+ }, {
247
+ transition: transitionSec
248
+ });
249
+ }
250
+ }
251
+ };
252
+ const runLights = (transitionSec, turnLightsOn) => {
253
+ if (currentSceneState == "Adaptive") {
254
+ runAdaptive(transitionSec, turnLightsOn);
255
+ }
256
+ else {
257
+ const scene = getSceneFromFriendlyName(currentSceneState);
258
+ if (!scene) {
259
+ self.error(`Scene not found: ${currentSceneState}`);
260
+ return;
261
+ }
262
+ activateScene(scene, transitionSec, turnLightsOn);
263
+ }
264
+ };
265
+ const runAdaptive = (transitionSec, turnLightsOn) => {
266
+ let entitiesOn;
267
+ let entitiesOff;
268
+ //Decide what scene to send
269
+ let scene = scenes["concentrate"];
270
+ if (nightModeState === "on") {
271
+ scene = scenes["night"];
272
+ entitiesOn = entitiesOnDuringNight;
273
+ entitiesOff = entitiesOffDuringNight;
274
+ }
275
+ else {
276
+ if (sunState == "above_horizon") {
277
+ scene = scenes["concentrate"];
278
+ }
279
+ else {
280
+ const currentHour = new Date().getHours();
281
+ if (currentHour >= 23 || currentHour < 6) {
282
+ scene = scenes["nightLight"];
283
+ }
284
+ else if (currentHour >= 6 && currentHour < 8) {
285
+ scene = scenes["relax"];
286
+ }
287
+ else if (currentHour >= 8 && currentHour < 10) {
288
+ scene = scenes["read"];
289
+ }
290
+ else if (currentHour >= 10 && currentHour < 15) {
291
+ scene = scenes["concentrate"];
292
+ }
293
+ else if (currentHour >= 15 && currentHour < 20) {
294
+ scene = scenes["relax"];
295
+ }
296
+ else if (currentHour >= 20 && currentHour < 23) {
297
+ scene = scenes["read"];
298
+ }
299
+ }
300
+ }
301
+ //Send it
302
+ activateScene(scene, transitionSec, turnLightsOn, entitiesOn, entitiesOff);
303
+ //Start our interval to update the adaptive scene every minute
304
+ clearTimeout(adaptiveInterval);
305
+ adaptiveInterval = setTimeout(() => {
306
+ runAdaptive(300, false);
307
+ }, 60000);
308
+ };
309
+ };
310
+ RED.nodes.registerType("light-control-config-node", register);
311
+ };
@@ -0,0 +1,30 @@
1
+ <script type="text/javascript" id="node-light-control-node">
2
+ RED.nodes.registerType("light-control-node", {
3
+ category: "HASS Stuff",
4
+ color: "#ffcc00",
5
+ inputs: 1,
6
+ outputs: 1,
7
+ icon: "bulb.svg",
8
+ defaults: {
9
+ name: { value: "" },
10
+ lightControlConfigNode: { value: '', type: "light-control-config-node", required: true }
11
+ },
12
+ label: function () {
13
+ return this.name || "Light Control"
14
+ }
15
+ });
16
+ </script>
17
+
18
+
19
+ <script type="text/html" data-template-name="light-control-node">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name" placeholder="Name">
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-input-lightControlConfigNode">Config</label>
26
+ <input type="text" id="node-input-lightControlConfigNode" />
27
+ </div>
28
+ </script>
29
+ <script type="text/html" data-help-name="light-control-node">
30
+ </script>
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ const baseNode_1 = require("../baseNode");
3
+ module.exports = function LightControlNode(RED) {
4
+ function register(config) {
5
+ RED.nodes.createNode(this, config);
6
+ (0, baseNode_1.assignBaseNode)(this);
7
+ const lightControlConfigNode = RED.nodes.getNode(config.lightControlConfigNode);
8
+ //When a config node wants to send a message to the output
9
+ lightControlConfigNode.addMsgCallback(this.id, (msg) => {
10
+ this.send(msg);
11
+ });
12
+ //When an input message is received
13
+ this.on("input", (msg, send, done) => {
14
+ lightControlConfigNode.msgReceived(msg, [this.id]);
15
+ });
16
+ }
17
+ RED.nodes.registerType("light-control-node", register);
18
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScenePresets = void 0;
4
+ var ScenePresets;
5
+ (function (ScenePresets) {
6
+ ScenePresets["rest"] = "e03267e7-9914-4f47-97fe-63c0bd317fe7";
7
+ ScenePresets["relax"] = "e71b2ef3-1b15-4c4b-b036-4b3d6efe58f8";
8
+ ScenePresets["read"] = "035b6ecf-414e-4781-abc7-3911556097cb";
9
+ ScenePresets["concentrate"] = "0cbec4e8-d064-4457-986a-fe6078a63f39";
10
+ ScenePresets["energize"] = "0eeacfc5-2d81-4035-a23d-4a9bc02af965";
11
+ ScenePresets["coolBright"] = "6d10a807-7330-46d1-b093-c15520ba72c0";
12
+ ScenePresets["bright"] = "84ebc26c-9d61-4d25-830c-41ea66f1c325";
13
+ ScenePresets["dimmed"] = "8f55e62a-e5f8-456a-9e8b-61f314bd4e99";
14
+ ScenePresets["nightLight"] = "b6f58e22-677f-4670-8677-3dea4ac60383";
15
+ ScenePresets["sleepy"] = "6170702c-f2fc-46fc-99ef-ac98b6c1293e";
16
+ ScenePresets["paintedSky"] = "467e2d6e-50e1-47eb-ac0f-ffd45d383a06";
17
+ ScenePresets["rollingHills"] = "49c61bae-d3ec-4df2-89a4-65235705f3a1";
18
+ ScenePresets["sailingAway"] = "f2ac2635-8b9e-4d5f-a307-63bd55e60475";
19
+ ScenePresets["sunFlare"] = "2f9ca2fb-dabb-4780-93cc-f9b169d94963";
20
+ })(ScenePresets = exports.ScenePresets || (exports.ScenePresets = {}));
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assignBaseConfigNode = void 0;
4
+ function assignBaseConfigNode(node) {
5
+ node.msgCallbacks = {};
6
+ node.addMsgCallback = function (id, callback) {
7
+ node.msgCallbacks[id] = callback;
8
+ };
9
+ node.removeMsgCallback = function (id) {
10
+ delete node.msgCallbacks[id];
11
+ };
12
+ node.sendMsg = function (msg, toIds) {
13
+ if (toIds) {
14
+ toIds.forEach(id => {
15
+ var _a, _b;
16
+ (_b = (_a = node.msgCallbacks)[id]) === null || _b === void 0 ? void 0 : _b.call(_a, msg);
17
+ });
18
+ return;
19
+ }
20
+ Object.values(node.msgCallbacks).forEach(callback => callback(msg));
21
+ };
22
+ node.msgReceived = function (msg, senderIds) { };
23
+ }
24
+ exports.assignBaseConfigNode = assignBaseConfigNode;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assignBaseNode = void 0;
4
+ function assignBaseNode(node) { }
5
+ exports.assignBaseNode = assignBaseNode;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getEntityId = exports.cleanEntityId = exports.generateRandomId = void 0;
4
+ function generateRandomId(checks) {
5
+ let id;
6
+ while (id === undefined || (checks && checks.includes(id))) {
7
+ id = Math.random().toString(36).substring(2, 15);
8
+ }
9
+ return id;
10
+ }
11
+ exports.generateRandomId = generateRandomId;
12
+ function cleanEntityId(entityId) {
13
+ return entityId.toLowerCase().replace(" - ", "_").replace(/\s/g, "_").replace(/[\W]+/g, "");
14
+ }
15
+ exports.cleanEntityId = cleanEntityId;
16
+ function getEntityId(scope, friendlyName) {
17
+ return `${scope}.${cleanEntityId(friendlyName)}`;
18
+ }
19
+ exports.getEntityId = getEntityId;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@haydendonald/node-red-contrib-hass-stuff",
3
+ "version": "0.0.1",
4
+ "description": "A collection of stuff I use on my Node Red + Home Assistant server. This could be of use for others, i don't know..",
5
+ "devDependencies": {
6
+ "@types/node": "^18.14.0",
7
+ "@types/node-red": "^1.2.1",
8
+ "typescript": "^4.9.5"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "LICENSE",
13
+ "package-lock.json",
14
+ "package.json",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "rm -rf dist && cp -r nodes dist && find dist -name '*.ts' -delete && tsc"
19
+ },
20
+ "keywords": [
21
+ "node-red"
22
+ ],
23
+ "author": "Hayden Donald",
24
+ "license": "ISC",
25
+ "node-red": {
26
+ "nodes": {
27
+ "ConnectionsConfigNode": "dist/Connections/connectionsConfigNode.js",
28
+ "ConnectionsNode": "dist/Connections/connectionsNode.js",
29
+ "LightControlConfigNode": "dist/LightControl/lightControlConfigNode.js",
30
+ "LightControlNode": "dist/LightControl/lightControlNode.js"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }