├── .dockerignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── __tests__ ├── lib │ └── timed-task.spec.js ├── mock-node.js └── nodes │ └── server-events-all.spec.js ├── docker ├── docker-compose.mapped.yml ├── docker-compose.yml ├── home-assistant │ ├── Dockerfile │ └── root-fs │ │ └── config │ │ └── configuration.yaml └── node-red │ ├── Dockerfile │ └── root-fs │ ├── app │ ├── nodemon.json │ └── package.json │ └── data │ ├── flows.json │ └── package.json ├── lib ├── base-node.js ├── events-node.js └── timed-task.js ├── nodes ├── api_call-service │ ├── api_call-service.html │ ├── api_call-service.js │ └── icons │ │ └── router-wireless.png ├── api_current-state │ ├── api_current-state.html │ ├── api_current-state.js │ └── icons │ │ └── arrow-top-right.png ├── api_get-history │ ├── api_get-history.html │ ├── api_get-history.js │ └── icons │ │ └── timer.png ├── api_render-template │ ├── api_render-template.html │ ├── api_render-template.js │ └── icons │ │ └── parser-json.png ├── config-server │ ├── config-server.html │ ├── config-server.js │ └── icons │ │ └── home.png ├── poll-state │ ├── icons │ │ └── timer.png │ ├── poll-state.html │ └── poll-state.js ├── server-events-all │ ├── icons │ │ └── arrow-right-bold.png │ ├── server-events-all.html │ └── server-events-all.js ├── server-events-state-changed │ ├── icons │ │ └── arrow-right-bold-hexagon-outline.png │ ├── server-events-state-changed.html │ └── server-events-state-changed.js └── trigger-state │ ├── icons │ └── trigger.png │ ├── trigger-state.html │ └── trigger-state.js ├── package-lock.json ├── package.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'standard', 4 | parserOptions: { sourceType: 'module' }, 5 | env: { browser: true }, 6 | rules: { 7 | 'arrow-parens': 0, 8 | 'space-before-function-paren': 0, 9 | 'no-warning-comments': [0, { 'terms': [ 'todo', 'fixme' ], 'location': 'start' }], 10 | 'generator-star-spacing': 0, 11 | semi: ['error', 'always', { 'omitLastInOneLineBlock': true }], 12 | // Because ocd 13 | 'standard/object-curly-even-spacing': 0, 14 | 'indent': [1, 4, { 15 | VariableDeclarator: { var: 1, let: 1, const: 1 } 16 | }], 17 | 'key-spacing': [1, { 'align': 'value' } ], 18 | 'no-multi-spaces': [ 0, { 19 | exceptions: { Property: true, VariableDeclarator: true, ImportDeclaration: true } 20 | }], 21 | 'max-len': [1, 200, 4, { ignoreComments: true }], 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Example Flow** 24 | If applicable, add an example of exported json of a flow exhibiting the issue to aid in reproduction. 25 | 26 | **Environment (please complete the following information):** 27 | - Node Red Version: [e.g. 0.18.4] 28 | - NR Home Assistant Plugin Version: [e.g. 0.3.0] 29 | - Is Node Red running in Docker: [e.g. yes/no] 30 | 31 | **Other (please complete the following information):** 32 | - Have you searched previous issues for duplicates?: 33 | - Did you attempt to reproduce the issue in the dev docker environment, if so what were results (See README.md): 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .env 3 | node_modules 4 | _scratchpad 5 | settings.js 6 | _scratchpad_flows 7 | .DS_Store 8 | _docker-volumes/ 9 | ./package-lock.json 10 | *.log 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach Docker: node-red", 9 | "type": "node", 10 | "request": "attach", 11 | "timeout": 20000, 12 | "restart": true, 13 | "address": "localhost", 14 | "port": 9123, 15 | "localRoot": "${workspaceFolder}", 16 | "remoteRoot": "/data/node_modules/node-red-contrib-home-assistant", 17 | "trace": false, 18 | "skipFiles": [ 19 | "/**/*.js" 20 | ] 21 | }, 22 | 23 | { 24 | "name": "Attach Local: node-red", 25 | "type": "node", 26 | "request": "attach", 27 | "timeout": 20000, 28 | "restart": true, 29 | "address": "localhost", 30 | "port": 9123, 31 | "trace": false, 32 | "skipFiles": [ 33 | "/**/*.js" 34 | ] 35 | }, 36 | { 37 | "name": "Attach Local: test", 38 | "type": "node", 39 | "request": "attach", 40 | "timeout": 20000, 41 | "restart": true, 42 | "address": "localhost", 43 | "port": 9124, 44 | "trace": false, 45 | 46 | "skipFiles": [ 47 | "/**/*.js" 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.3.2 2 | * Feat: History node now supports end date and entityid filtering 3 | * Fix: Nodered configuration value userDir is fetched correctly now 4 | 5 | # v0.3.1 6 | * Fix/Feat: Current State Node: Preserve original message with options to override topic and payload (Thanks @declankenny) 7 | * Fix: Added try/catch in onHaEventsStateChanged (Thanks @oskargronqvist) 8 | * Docs: Added quotes to 'Call Service' placeholder text (Thanks @brubaked) 9 | * Docs: Readme node version clarification (Thanks @Bodge-IT) 10 | * Docs: Keyword discoverability addition (Thanks @jcullen86) 11 | * Docs: Note of server URL when running inside container (Thanks @drogfild) 12 | * Dev: Updated docker dev environment to latest, easier setup and run, added some tests within node-red sample flows 13 | * Chore: Added LICENSE.md 14 | * Chore: Added Feature and Bug templates 15 | * Chore: Added CONTRIBUTING.md 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Node Red Home Assistant 2 | 3 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Using the issue tracker 8 | The issue tracker is the preferred channel for [bug reports](#bugs), [features requests](#features) and [submitting pull requests](#pull-requests). Currently [support issues](#support-issues) are acceptable in the isuse tracker as well although this may change in the future to cut down on the noise. 9 | 10 | ## Bug reports 11 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful - thank you! 12 | 13 | Guidelines for bug reports: 14 | 15 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 16 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or `dev-master` branch in the repository. 17 | 3. **Isolate the problem** — ideally create a reduced test case, an example flow in node-red that you can add to the issue is preferred. 18 | 19 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What OS experiences the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs. Fill out all the information you can when presented with the Bug Issue template. 20 | 21 | ## Feature requests 22 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. 23 | 24 | 25 | ## Pull requests 26 | Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. 27 | 28 | **Please note: All pull requests should be submitted against the `dev-master` branch, this allows for some manual testing prior to a relase.** 29 | 30 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. 31 | 32 | ### For new Contributors 33 | 34 | If you never created a pull request before, welcome :tada: :smile: [Here is a great tutorial](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) on how to send one :) 35 | 36 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes: 37 | 38 | ```bash 39 | # Clone your fork of the repo into the current directory 40 | git clone https://github.com// 41 | # Navigate to the newly cloned directory 42 | cd 43 | # Assign the original repo to a remote called "upstream" 44 | git remote add upstream https://github.com/ayapejian/node-red-contrib-home-assistant 45 | ``` 46 | 47 | 2. If you cloned a while ago, get the latest changes from upstream: 48 | 49 | ```bash 50 | git checkout master 51 | git pull upstream master 52 | ``` 53 | 54 | 3. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix: 55 | 56 | ```bash 57 | git checkout -b 58 | ``` 59 | 60 | 4. If you added or changed a feature, make sure to document it accordingly in the `README.md` file. 61 | 62 | 6. Push your topic branch up to your fork: 63 | 64 | ```bash 65 | git push origin 66 | ``` 67 | 68 | 8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description. 69 | 70 | 71 | ## Support Issues 72 | At some point, if the need is apparent, the project may get another venue to foster disucssion and help each other with support issues at which point support issues will no longer be accepted in the github issues interface. Until that happens feel free to submit your support issues if they are unique and you are stuck. 73 | 74 | Examples of support issues are thing like server communication errors, something that only happens "in your environment" (not directly code related). A lot of support issues have already been logged and solved so make sure to search previous issues (including closed issues) first and also read the README.md file here. If applicable please fully document how the issue was resolved, this will help others in the future. 75 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) <2018> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Red Contrib Home Assistant 2 | 3 | Various nodes to assist in setting up automation using [node-red](https://nodered.org/) communicating with [Home Assistant](https://home-assistant.io/). 4 | 5 | ## Project status 6 | 7 | Project is going through active development and as such will probably have a few 'growing pain' bugs as well as node type, input, output and functionality changes. At this stage backwards compatibility between versions is not a main concern and a new version __may mean you'll have to recreate certain nodes.__ 8 | 9 | ## Getting Started 10 | 11 | This assumes you have [node-red](http://nodered.org/) already installed and working, if you need to install node-red see [here](http://nodered.org/docs/getting-started/installation) 12 | 13 | NOTE: node-red-contrib-home-assistant requires node.JS > 8.0 If you're running Node-Red in Docker you'll need to pull the -v8 image for this to work. 14 | 15 | ```shell 16 | $ cd cd ~/.node-red 17 | $ npm install node-red-contrib-home-assistant 18 | # then restart node-red 19 | ``` 20 | 21 | If you are running Node Red inside Hass.io addon/container you can use Hass.io API Proxy address `http://hassio/homeassistant` as Home Assistant server address (server node Base URL). This way you don't need any real network address. 22 | 23 | ======= 24 | For flow examples checkout the [flows here](https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/master/_docker/node-red/root-fs/data/flows.json) 25 | 26 | --- 27 | ## Included Nodes 28 | The installed nodes have more detailed information in the node-red info pane shown when the node is selected. Below is a quick summary 29 | 30 | ### All Events 31 | Listens for all types of events from home assistant 32 | 33 | ### State Changed Event 34 | Listens for only `state_changed` events from home assistant 35 | 36 | ### State Trigger 37 | Much like the `State Changed Ndoe` however provides some advanced functionality around common automation use cases. 38 | 39 | ### Poll State 40 | Outputs the state of an entity at regular intervals, optionally also at startup and every time the entity changes if desired 41 | 42 | ### Call Service 43 | Sends a request to home assistant for any domain and service available ( `light/turn_on`, `input_select/select_option`, etc..) 44 | 45 | ### Get Current State 46 | Fetches the last known state for any entity on input 47 | 48 | ### Get History 49 | Fetches HomeAssistant history on input 50 | 51 | ### Get Template 52 | Allows rendering of templates on input 53 | 54 | --- 55 | ## Development 56 | An environment with Home Assistant/Node Red can be easily spun up using docker and docker-compose along with built in VSCode debug enabled. 57 | 58 | 1. Clone this repository: `git clone https://github.com/AYapejian/node-red-contrib-home-assistant.git` 59 | 2. Install node dependencies as usual: `cd node-red-contrib-home-assistant && yarn` 60 | 3. Start the docker dev environment: `yarn run dev` 61 | a. _Note: First run will take a bit to download the images ( home-assistants image is over 1gb (yikes!) after that launch is much quicker)_ 62 | b. _Note: Also first run load of HomeAssistant web interface seems very slow, but after first time it's also much faster_ 63 | 4. The `yarn run dev` command will leave you with a terminal spitting out logs, `ctrl+c` out of this and it kills all the servers by design, just run `yarn run dev` again to pick back up. The following services and ports are launched in the `dev` script 64 | 65 | 66 | | service | port mappings | info | 67 | |------------------------|--------------------------|---------------------------------------------------------------------------------------------------------------------------------| 68 | | home-assistant | `8123:8123`, `8300:8300` | exposed for local access via browser | 69 | | node-red | `1880:1880`, `9123:9229` | exposed for local access via browser, `9123` is used for debugging. Includes default flow example connected to `home-assistant` | | 70 | 71 | ### Docker Tips 72 | 1. If you run into environment issues running `yarn run dev:clean` should remove all docker data and get you back to a clean state 73 | 2. All data will be discarded when the docker container is removed. You can map volumes locally to persist data. Create and copy as directed below then modify `docker-compose.yaml` to map the container directories to the created host dirs below. _See: `./_docker/docker-compose.mapped.yaml` for an example or just use that file to launch manually_ 74 | 75 | ``` 76 | mkdir -p _docker-volumes/home-assistant/config 77 | mkdir -p _docker-volumes/node-red/data 78 | cp _docker/home-assistant/root-fs/config/* _docker-volumes/home-assistant/config/ 79 | cp _docker/node-red/root-fs/data/* _docker-volumes/node-red/data 80 | ``` 81 | 82 | ### Node Debugger via VSCode 83 | Optional but it's pretty nice if you have VSCode installed. 84 | - Open the project directory in VSCode 85 | - Go to the debug tab ( or `cmd/ctrl+shift+d`) 86 | - In the debug tab you should see an target for "Attach: Docker", run that guy and you can place debug breakpoints and changes will be reloaded within docker automatically 87 | - Open [http://localhost:8123](http://localhost:8123) for HomeAssistant (password is `password` by default). 88 | - For node-red either open up via the HomeAssistant web link or left hand menu or just open a browser tab to [http://localhost:1880](http://localhost:1880) 89 | 90 | ### Other Dev Tips 91 | * If you're using VSCode and annoyed that node-red html ( `type="x-red"` ) isn't syntax highlighted you can run force it by adding support. Below is for Mac, can do the same manually on any platform however, note that this is a hack as I couldn't find any other good way to do this. 92 | 93 | ```shell 94 | # For VSCode 95 | sed -i .orig 's/text\/(javascript|ecmascript|babel)/text\/(javascript|ecmascript|babel|x-red)/' "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/html/syntaxes/html.json" 96 | 97 | # For VSCode Insiders 98 | sed -i .orig 's/text\/(javascript|ecmascript|babel)/text\/(javascript|ecmascript|babel|x-red)/' "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions/html/syntaxes/html.json" 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /__tests__/lib/timed-task.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const TimedTask = require('../../lib/timed-task'); 3 | 4 | const NOW = 0; 5 | 6 | TimedTask.prototype.getNow = () => NOW; 7 | 8 | const options = () => ({ 9 | id: 'my timed task', 10 | runsIn: 1, 11 | task: () => '1', 12 | onStart: () => '2', 13 | onComplete: () => '3', 14 | onCancel: () => '4', 15 | onFailed: (e) => '5' 16 | }); 17 | 18 | test('TimedTask: should instantiate', function(t) { 19 | const opts = options(); 20 | const tt = new TimedTask(opts); 21 | t.equal(tt.id, opts.id, 'ids should work'); 22 | t.equal(tt.runsAt, NOW + opts.runsIn, 'runsAt should be calculated'); 23 | t.equal(typeof tt.task, 'function'); 24 | t.end(); 25 | }); 26 | 27 | test('TimedTask: should run task', function(t) { 28 | const opts = options(); 29 | const onDone = () => t.end(); 30 | opts.task = () => (t.pass('task called when scheduled'), onDone()); // eslint-disable-line 31 | new TimedTask(opts); // eslint-disable-line 32 | }); 33 | 34 | test('TimedTask: should call onStart and onComplete', function(t) { 35 | const opts = options(); 36 | 37 | const onDone = () => t.end(); 38 | opts.onStart = () => t.pass('onStart called'); 39 | opts.onComplete = () => (t.pass('on complete called'), onDone()); // eslint-disable-line 40 | 41 | new TimedTask(opts); // eslint-disable-line 42 | }); 43 | 44 | test('TimedTask: should serialize', function(t) { 45 | const tt = new TimedTask(options()); 46 | const str = tt.serialize(); 47 | const expectedStr = `{"id":"my timed task","runsAt":1,"task":() => '1',"onStart":() => '2',"onComplete":() => '3',"onCancel":() => '4',"onFailed":(e) => '5'}`; 48 | t.equal(str, expectedStr, 'serialized string is correct'); 49 | t.end(); 50 | }); 51 | 52 | test('TimedTask: should deserialize', function(t) { 53 | const opts = options(); 54 | const tt = new TimedTask(opts); 55 | const str = tt.serialize(); 56 | const timedTask = TimedTask.createFromSerialized(str); 57 | 58 | t.equals(timedTask.id, opts.id, 'deserialized id is equal'); 59 | t.equals(typeof timedTask.task, 'function', 'task function is deserialized correctly'); 60 | 61 | t.end(); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/mock-node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // var assert = require('assert'); 3 | 4 | // class Context { 5 | // constructor(type) { 6 | // this._values = {}; 7 | // this._type = type; 8 | // } 9 | // get(key) { 10 | // return this._values[key]; 11 | // } 12 | // set(key, value) { 13 | // console.log(this._type + ' context: set [' + key + '] => [' + value + ']'); 14 | // this._values[key] = value; 15 | // } 16 | // } 17 | 18 | class MockNode { 19 | constructor() { 20 | this._events = {}; 21 | this._state = {}; 22 | this._sent = []; 23 | this._context = {}; 24 | this._status = {}; 25 | } 26 | 27 | log() { console.log(...arguments) } 28 | warn() { console.log(...arguments) } 29 | error() { console.log(...arguments) } 30 | on(event, eventFn) { 31 | this._events[event] = eventFn; 32 | } 33 | emit(event, data) { 34 | this._events[event].call(this, data); 35 | } 36 | status(status) { 37 | if (status) this._status = status; 38 | return status; 39 | } 40 | send(msg) { 41 | this._sent.push(msg); 42 | } 43 | sent(index) { 44 | return this._sent[index]; 45 | } 46 | context() { 47 | return this._context; 48 | } 49 | } 50 | 51 | module.exports = function(nodeRedModule, config) { 52 | const RED = { 53 | nodes: { 54 | getNode: () => {}, 55 | registerType: function(nodeName, nodeConfigFunc) { 56 | this.nodeConfigFunc = nodeConfigFunc; 57 | }, 58 | createNode: function() { 59 | // TODO write me 60 | } 61 | }, 62 | settings: { 63 | get: () => {} 64 | }, 65 | comms: { 66 | publish: () => true 67 | } 68 | }; 69 | const mockNode = new MockNode(); 70 | // Register the node (calls registerType) 71 | nodeRedModule(RED); 72 | return new RED.nodes.nodeConfigFunc.call(mockNode, config); // eslint-disable-line 73 | }; 74 | -------------------------------------------------------------------------------- /__tests__/nodes/server-events-all.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const helper = require('node-red/test/nodes/helper'); 3 | const ConfigServerNode = require('../../nodes/config-server/config-server'); 4 | const ServerEventsNode = require('../../nodes/server-events-all/server-events-all'); 5 | 6 | test('before: start-server', function(t) { 7 | helper.startServer(() => t.end()); 8 | }); 9 | 10 | test('Simple Node: should load', function(t) { 11 | let flow = [ 12 | { id: 'n1', type: 'server-events', server: 'n2', wires: [] }, 13 | { id: 'n2', type: 'server', name: 'ha-server', url: 'http://localhost:1234', pass: '123' } 14 | ]; 15 | 16 | helper.load([ServerEventsNode, ConfigServerNode], flow, function() { 17 | const n1 = helper.getNode('n1'); 18 | t.equals(n1.type, 'server-events', 'simple node should instantiate with type "simple-node"'); 19 | helper.unload(); 20 | t.end(); 21 | }); 22 | }); 23 | 24 | // test('Simple Node: should send lowercased payload', function(t) { 25 | // let flow = [ 26 | // { id: 'n1', type: 'simple-node', server: 'n2', wires: [ ['n3'] ] }, 27 | // { id: 'n2', type: 'config-node', host: 'localhost', port: '1234' }, 28 | // { id: 'n3', type: 'helper' } 29 | // ]; 30 | 31 | // helper.load([ConfigNode, SimpleNode], flow, function() { 32 | // const n1 = helper.getNode('n1'); 33 | // const n3 = helper.getNode('n3'); 34 | 35 | // n3.on('input', function(msg) { 36 | // t.equals(msg.topic, expectedMsg.topic, 'topics should match'); 37 | // t.equals(msg.payload, expectedMsg.payload.toLowerCase(), 'payload should be lowercased match'); 38 | // t.end(); 39 | // }); 40 | 41 | // const expectedMsg = { topic: 'test', payload: 'ABC' }; 42 | // n1.receive(expectedMsg); 43 | // }); 44 | // }); 45 | 46 | test('after: stop-server', function(t) { 47 | helper.stopServer(); 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /docker/docker-compose.mapped.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | node-red: 5 | container_name: node-red 6 | env_file: ./config.env 7 | build: 8 | context: ./node-red 9 | command: npm run dev:watch 10 | volumes: 11 | - '..:/data/node_modules/node-red-contrib-home-assistant' 12 | - '../_docker-volumes/node-red/data/flows.json:/data/flows.json' 13 | - '../_docker-volumes/node-red/data/settings.js:/data/settings.js' 14 | - '../_docker-volumes/node-red/data/settings.js:/data/package.json' 15 | ports: 16 | - 1880:1880 17 | - 9123:9229 18 | 19 | home-assistant: 20 | container_name: home-assistant 21 | env_file: ./config.env 22 | build: 23 | context: ./home-assistant 24 | volumes: 25 | - '../_docker-volumes/home-assistant/config/configuration.yaml:/config/configuration.yaml' 26 | ports: 27 | - 8300:8300 28 | - 8123:8123 29 | 30 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node-red: 5 | build: ./node-red 6 | command: npm run dev:watch 7 | volumes: 8 | - '..:/data/node_modules/node-red-contrib-home-assistant' 9 | # - './node-red/root-fs/data/flows.json:/data/flows.json' # Map flows file for easy commitable examples building 10 | ports: 11 | - 1880:1880 12 | - 9123:9229 13 | 14 | home-assistant: 15 | build: ./home-assistant 16 | # While messing with HA config map the repo config to the container for easy changes 17 | volumes: 18 | - './home-assistant/root-fs/config/configuration.yaml:/config/configuration.yaml' 19 | ports: 20 | - 8300:8300 21 | - 8123:8123 22 | -------------------------------------------------------------------------------- /docker/home-assistant/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM homeassistant/home-assistant:0.67.1 2 | 3 | ENV HA_HOME_ASSISTANT_TIME_ZONE=America/New_York \ 4 | HA_HOME_ASSISTANT_LATITUDE=42.360082 \ 5 | HA_HOME_ASSISTANT_LONGITUDE=-71.058880 \ 6 | HA_HOME_ASSISTANT_ELEVATION=38 \ 7 | HA_HOME_ASSISTANT_TEMPERATURE_UNIT=F \ 8 | HA_HOME_ASSISTANT_UNIT_SYSTEM=imperial \ 9 | HA_HTTP_API_PASSWORD=password \ 10 | HA_LOGGER_DEFAULT=info \ 11 | NODE_RED_URL=http://localhost:1880/ 12 | 13 | COPY root-fs / 14 | 15 | CMD [ "python", "-m", "homeassistant", "--config", "/config" ] 16 | -------------------------------------------------------------------------------- /docker/home-assistant/root-fs/config/configuration.yaml: -------------------------------------------------------------------------------- 1 | homeassistant: 2 | name: Home 3 | latitude: !env_var HA_HOME_ASSISTANT_LATITUDE 4 | longitude: !env_var HA_HOME_ASSISTANT_LONGITUDE 5 | elevation: !env_var HA_HOME_ASSISTANT_ELEVATION 6 | temperature_unit: !env_var HA_HOME_ASSISTANT_TEMPERATURE_UNIT 7 | time_zone: !env_var HA_HOME_ASSISTANT_TIME_ZONE 8 | unit_system: !env_var HA_HOME_ASSISTANT_UNIT_SYSTEM 9 | 10 | http: 11 | api_password: !env_var HA_HTTP_API_PASSWORD 12 | 13 | logger: 14 | default: !env_var HA_LOGGER_DEFAULT 15 | 16 | recorder: 17 | logbook: 18 | frontend: 19 | config: 20 | history: 21 | map: 22 | sun: 23 | 24 | panel_iframe: 25 | node_red: 26 | title: Node Red 27 | url: !env_var NODE_RED_URL 28 | icon: mdi:sitemap 29 | 30 | 31 | alarm_control_panel: 32 | - platform: demo 33 | 34 | binary_sensor: 35 | - platform: demo 36 | 37 | camera: 38 | - platform: demo 39 | 40 | climate: 41 | - platform: demo 42 | 43 | cover: 44 | - platform: demo 45 | 46 | fan: 47 | - platform: demo 48 | 49 | image_processing: 50 | - platform: demo 51 | 52 | light: 53 | - platform: demo 54 | 55 | lock: 56 | - platform: demo 57 | 58 | notify: 59 | - platform: demo 60 | 61 | remote: 62 | - platform: demo 63 | 64 | sensor: 65 | - platform: demo 66 | 67 | switch: 68 | - platform: demo 69 | 70 | tts: 71 | - platform: demo 72 | 73 | weather: 74 | - platform: demo 75 | 76 | mailbox: 77 | - platform: demo 78 | 79 | input_select: 80 | time_of_day: 81 | name: Time of Day 82 | options: 83 | - Morning 84 | - Afternoon 85 | - Evening 86 | - Party Time 87 | initial: Morning 88 | icon: mdi:schedule 89 | 90 | input_number: 91 | global_light_level: 92 | name: Global Light Level 93 | initial: 10 94 | min: 1 95 | max: 100 96 | step: 5 97 | 98 | input_boolean: 99 | notify_home: 100 | name: Use Global Light Level 101 | initial: off 102 | icon: mdi:lightbulb_outline 103 | 104 | input_datetime: 105 | only_date: 106 | name: Input with only date 107 | has_date: true 108 | has_time: false 109 | only_time: 110 | name: Input with only time 111 | has_date: false 112 | has_time: true 113 | 114 | # Test Entities for Development 115 | # automation: 116 | # initial_state: True 117 | # trigger: 118 | # platform: time 119 | # seconds: '/10' 120 | # action: 121 | # service: switch.toggle 122 | # entity_id: light.kitchen_light 123 | -------------------------------------------------------------------------------- /docker/node-red/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.9.4 2 | 3 | COPY root-fs / 4 | RUN chown -R node:node /data /app 5 | 6 | # Install NodeRed base app 7 | ENV HOME=/app 8 | USER node 9 | WORKDIR /app 10 | RUN npm install 11 | 12 | # Install Useful Home Automation Nodes 13 | RUN npm install \ 14 | node-red-contrib-bigstatus 15 | 16 | # User configuration directory volume 17 | EXPOSE 1880 18 | EXPOSE 9229 19 | 20 | # Environment variable holding file path for flows configuration 21 | ENV USER_DIR=/data \ 22 | FLOWS=flows.json \ 23 | NODE_PATH=/app/node_modules:/data/node_modules \ 24 | NODEMON_CONFIG=/app/nodemon.json \ 25 | NODE_ENV=development \ 26 | DEBUG=home-assistant* 27 | 28 | CMD ["npm", "start"] 29 | -------------------------------------------------------------------------------- /docker/node-red/root-fs/app/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "colours": true, 3 | "delay": "300", 4 | "verbose": false, 5 | "watch": [ 6 | "/data/node_modules/node-red-contrib-home-assistant" 7 | ], 8 | "ignore": [ 9 | "/data/node_modules/node-red-contrib-home-assistant/node_modules", 10 | "/data/node_modules/node-red-contrib-home-assistant/_docker", 11 | "/data/node_modules/node-red-contrib-home-assistant/_docker-volumes" 12 | ], 13 | "env": { 14 | "NODE_ENV": "development" 15 | }, 16 | "ext": "js,json,html", 17 | "ignoreRoot": [ 18 | "**/.git/**", 19 | "**/.nyc_output/**", 20 | "**/.sass-cache/**", 21 | "**/bower_components/**", 22 | "**/coverage/**" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /docker/node-red/root-fs/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-home-assistant-docker", 3 | "version": "1.0.0", 4 | "description": "Node red install for docker, tailored for use with Home Assistant", 5 | "homepage": "https://github.com/ayapejian/node-red-contrib-home-assistant", 6 | "main": "node_modules/node-red/red/red.js", 7 | "scripts": { 8 | "start": "node $NODE_OPTIONS /app/node_modules/node-red/red.js -v --userDir $USER_DIR $FLOWS", 9 | "dev:watch": "/app/node_modules/.bin/nodemon --config $NODEMON_CONFIG --exec 'sleep 0.5; node --inspect=0.0.0.0:9229 /app/node_modules/node-red/red.js -v --userDir $USER_DIR $FLOWS'" 10 | }, 11 | "dependencies": { 12 | "node-red": "0.18.4", 13 | "node-red-contrib-contextbrowser": "*", 14 | "nodemon": "1.14.11" 15 | }, 16 | "engines": { 17 | "node": "8.*.*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker/node-red/root-fs/data/flows.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "99942b96.728d68", 4 | "type": "tab", 5 | "label": "Events All & State Changed", 6 | "disabled": false, 7 | "info": "Tests for 'events: all' and 'events: state' nodes" 8 | }, 9 | { 10 | "id": "63d2914d.5ea9b", 11 | "type": "tab", 12 | "label": "Call Service", 13 | "disabled": false, 14 | "info": "" 15 | }, 16 | { 17 | "id": "dc41607f.a08f2", 18 | "type": "tab", 19 | "label": "Current State", 20 | "disabled": false, 21 | "info": "" 22 | }, 23 | { 24 | "id": "ae2a9159.be871", 25 | "type": "tab", 26 | "label": "History & Template", 27 | "disabled": false, 28 | "info": "" 29 | }, 30 | { 31 | "id": "683ec059.246e5", 32 | "type": "tab", 33 | "label": "Poll State", 34 | "disabled": false, 35 | "info": "" 36 | }, 37 | { 38 | "id": "52eaaa74.1d85e4", 39 | "type": "tab", 40 | "label": "Trigger Node", 41 | "disabled": false, 42 | "info": "" 43 | }, 44 | { 45 | "id": "c81c02fc.f75a5", 46 | "type": "tab", 47 | "label": "Using HA Context", 48 | "disabled": false, 49 | "info": "" 50 | }, 51 | { 52 | "id": "4780c192.bc0f2", 53 | "type": "server", 54 | "z": "", 55 | "name": "Home Assistant", 56 | "url": "http://home-assistant:8123", 57 | "pass": "password" 58 | }, 59 | { 60 | "id": "f31326a2.18c1e8", 61 | "type": "server-events", 62 | "z": "99942b96.728d68", 63 | "name": "Test", 64 | "server": "4780c192.bc0f2", 65 | "x": 130, 66 | "y": 100, 67 | "wires": [ 68 | [ 69 | "1f11ccc0.c9a1c3" 70 | ] 71 | ] 72 | }, 73 | { 74 | "id": "1f11ccc0.c9a1c3", 75 | "type": "debug", 76 | "z": "99942b96.728d68", 77 | "name": "", 78 | "active": true, 79 | "tosidebar": true, 80 | "console": false, 81 | "tostatus": false, 82 | "complete": "true", 83 | "x": 330, 84 | "y": 100, 85 | "wires": [] 86 | }, 87 | { 88 | "id": "d1c61340.98165", 89 | "type": "server-state-changed", 90 | "z": "99942b96.728d68", 91 | "name": "Substring 'sun'", 92 | "server": "4780c192.bc0f2", 93 | "entityidfilter": "sun", 94 | "entityidfiltertype": "substring", 95 | "haltifstate": "", 96 | "x": 160, 97 | "y": 160, 98 | "wires": [ 99 | [ 100 | "879548f5.5419c8" 101 | ] 102 | ] 103 | }, 104 | { 105 | "id": "879548f5.5419c8", 106 | "type": "debug", 107 | "z": "99942b96.728d68", 108 | "name": "", 109 | "active": true, 110 | "tosidebar": false, 111 | "console": false, 112 | "tostatus": true, 113 | "complete": "payload", 114 | "x": 350, 115 | "y": 160, 116 | "wires": [] 117 | }, 118 | { 119 | "id": "c018ca12.9b0f18", 120 | "type": "server-state-changed", 121 | "z": "99942b96.728d68", 122 | "name": "Regex 'sun'", 123 | "server": "4780c192.bc0f2", 124 | "entityidfilter": "sun", 125 | "entityidfiltertype": "regex", 126 | "haltifstate": "", 127 | "x": 150, 128 | "y": 280, 129 | "wires": [ 130 | [ 131 | "cb214148.637d4" 132 | ] 133 | ] 134 | }, 135 | { 136 | "id": "cb214148.637d4", 137 | "type": "debug", 138 | "z": "99942b96.728d68", 139 | "name": "", 140 | "active": true, 141 | "tosidebar": false, 142 | "console": false, 143 | "tostatus": true, 144 | "complete": "payload", 145 | "x": 350, 146 | "y": 280, 147 | "wires": [] 148 | }, 149 | { 150 | "id": "71fcdb8d.4bbc44", 151 | "type": "server-state-changed", 152 | "z": "99942b96.728d68", 153 | "name": "Regex 's.n'", 154 | "server": "4780c192.bc0f2", 155 | "entityidfilter": "s.n", 156 | "entityidfiltertype": "regex", 157 | "haltifstate": "", 158 | "x": 140, 159 | "y": 380, 160 | "wires": [ 161 | [ 162 | "75d24d67.f9edb4" 163 | ] 164 | ] 165 | }, 166 | { 167 | "id": "75d24d67.f9edb4", 168 | "type": "debug", 169 | "z": "99942b96.728d68", 170 | "name": "", 171 | "active": true, 172 | "tosidebar": false, 173 | "console": false, 174 | "tostatus": true, 175 | "complete": "payload", 176 | "x": 370, 177 | "y": 380, 178 | "wires": [] 179 | }, 180 | { 181 | "id": "708d9765.ad5dc8", 182 | "type": "server-state-changed", 183 | "z": "99942b96.728d68", 184 | "name": "Exact 'sun.sun'", 185 | "server": "4780c192.bc0f2", 186 | "entityidfilter": "sun.sun", 187 | "entityidfiltertype": "exact", 188 | "haltifstate": "", 189 | "x": 160, 190 | "y": 220, 191 | "wires": [ 192 | [ 193 | "6f2a2bd4.265ea4" 194 | ] 195 | ] 196 | }, 197 | { 198 | "id": "6f2a2bd4.265ea4", 199 | "type": "debug", 200 | "z": "99942b96.728d68", 201 | "name": "", 202 | "active": true, 203 | "tosidebar": false, 204 | "console": false, 205 | "tostatus": true, 206 | "complete": "payload", 207 | "x": 350, 208 | "y": 220, 209 | "wires": [] 210 | }, 211 | { 212 | "id": "4ef7a108.d71d6", 213 | "type": "api-call-service", 214 | "z": "63d2914d.5ea9b", 215 | "name": "Time of Day: Select Next", 216 | "server": "4780c192.bc0f2", 217 | "service_domain": "input_select", 218 | "service": "select_next", 219 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 220 | "mergecontext": "", 221 | "x": 530, 222 | "y": 100, 223 | "wires": [ 224 | [ 225 | "ea2f46e7.871258" 226 | ] 227 | ] 228 | }, 229 | { 230 | "id": "fdd917f1.fb3a08", 231 | "type": "inject", 232 | "z": "63d2914d.5ea9b", 233 | "name": "use node config defaults", 234 | "topic": "", 235 | "payload": "", 236 | "payloadType": "date", 237 | "repeat": "", 238 | "crontab": "", 239 | "once": false, 240 | "x": 250, 241 | "y": 100, 242 | "wires": [ 243 | [ 244 | "4ef7a108.d71d6" 245 | ] 246 | ] 247 | }, 248 | { 249 | "id": "9c79a3e.398646", 250 | "type": "inject", 251 | "z": "63d2914d.5ea9b", 252 | "name": "override svc: 'select previous'", 253 | "topic": "", 254 | "payload": "{\"service\":\"select_previous\"}", 255 | "payloadType": "json", 256 | "repeat": "", 257 | "crontab": "", 258 | "once": false, 259 | "x": 240, 260 | "y": 160, 261 | "wires": [ 262 | [ 263 | "eba49a.66b6cb68" 264 | ] 265 | ] 266 | }, 267 | { 268 | "id": "6b4388db.79cf88", 269 | "type": "server-state-changed", 270 | "z": "63d2914d.5ea9b", 271 | "name": "Time of Day: On Change", 272 | "server": "4780c192.bc0f2", 273 | "entityidfilter": "input_select.time_of_day", 274 | "entityidfiltertype": "substring", 275 | "haltifstate": "", 276 | "x": 1410, 277 | "y": 80, 278 | "wires": [ 279 | [ 280 | "8544614e.23128" 281 | ] 282 | ] 283 | }, 284 | { 285 | "id": "8544614e.23128", 286 | "type": "debug", 287 | "z": "63d2914d.5ea9b", 288 | "name": "", 289 | "active": true, 290 | "tosidebar": false, 291 | "console": false, 292 | "tostatus": true, 293 | "complete": "payload", 294 | "x": 1640, 295 | "y": 80, 296 | "wires": [] 297 | }, 298 | { 299 | "id": "8f15f93e.b542d8", 300 | "type": "inject", 301 | "z": "63d2914d.5ea9b", 302 | "name": "override svc & merge data: 'party time' ", 303 | "topic": "", 304 | "payload": "{\"service\":\"select_option\",\"data\":{\"option\":\"Party Time\"}}", 305 | "payloadType": "json", 306 | "repeat": "", 307 | "crontab": "", 308 | "once": false, 309 | "x": 210, 310 | "y": 220, 311 | "wires": [ 312 | [ 313 | "f59a907f.7f08b8" 314 | ] 315 | ] 316 | }, 317 | { 318 | "id": "70a67972.83cc58", 319 | "type": "api-call-service", 320 | "z": "63d2914d.5ea9b", 321 | "name": "time of day: select option using context", 322 | "server": "4780c192.bc0f2", 323 | "service_domain": "input_select", 324 | "service": "select_option", 325 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 326 | "mergecontext": "timeOfDayOption", 327 | "x": 960, 328 | "y": 400, 329 | "wires": [ 330 | [ 331 | "9175adc1.7cf5c" 332 | ] 333 | ] 334 | }, 335 | { 336 | "id": "79d6048a.91be0c", 337 | "type": "inject", 338 | "z": "63d2914d.5ea9b", 339 | "name": "run", 340 | "topic": "", 341 | "payload": "", 342 | "payloadType": "date", 343 | "repeat": "", 344 | "crontab": "", 345 | "once": false, 346 | "x": 110, 347 | "y": 400, 348 | "wires": [ 349 | [ 350 | "6f4771cd.9bd3a" 351 | ] 352 | ] 353 | }, 354 | { 355 | "id": "7a39148b.a7576c", 356 | "type": "function", 357 | "z": "63d2914d.5ea9b", 358 | "name": "set GLOBAL ctx time of day 'Afternoon'", 359 | "func": "global.set('timeOfDayOption', {\n option: 'Afternoon'\n});\n\nreturn msg;", 360 | "outputs": 1, 361 | "noerr": 0, 362 | "x": 600, 363 | "y": 400, 364 | "wires": [ 365 | [ 366 | "70a67972.83cc58" 367 | ] 368 | ] 369 | }, 370 | { 371 | "id": "1f371962.dd54c7", 372 | "type": "inject", 373 | "z": "63d2914d.5ea9b", 374 | "name": "run", 375 | "topic": "", 376 | "payload": "", 377 | "payloadType": "date", 378 | "repeat": "", 379 | "crontab": "", 380 | "once": false, 381 | "x": 110, 382 | "y": 440, 383 | "wires": [ 384 | [ 385 | "ab945ee0.48ea2" 386 | ] 387 | ] 388 | }, 389 | { 390 | "id": "e2448125.9980f", 391 | "type": "function", 392 | "z": "63d2914d.5ea9b", 393 | "name": "set FLOW ctx time of day 'Morning'", 394 | "func": "flow.set('timeOfDayOption', {\noption: 'Morning'\n});\n\nreturn msg;", 395 | "outputs": 1, 396 | "noerr": 0, 397 | "x": 580, 398 | "y": 440, 399 | "wires": [ 400 | [ 401 | "2517417.5e2073e" 402 | ] 403 | ] 404 | }, 405 | { 406 | "id": "6f4771cd.9bd3a", 407 | "type": "function", 408 | "z": "63d2914d.5ea9b", 409 | "name": "clear flow and global ctx", 410 | "func": "flow.set('timeOfDayOption', null);\nglobal.set('timeOfDayOption', null);\n\nreturn msg;", 411 | "outputs": 1, 412 | "noerr": 0, 413 | "x": 310, 414 | "y": 400, 415 | "wires": [ 416 | [ 417 | "7a39148b.a7576c" 418 | ] 419 | ] 420 | }, 421 | { 422 | "id": "ab945ee0.48ea2", 423 | "type": "function", 424 | "z": "63d2914d.5ea9b", 425 | "name": "clear flow and global ctx", 426 | "func": "flow.set('timeOfDayOption', null);\nglobal.set('timeOfDayOption', null);\n\nreturn msg;", 427 | "outputs": 1, 428 | "noerr": 0, 429 | "x": 310, 430 | "y": 440, 431 | "wires": [ 432 | [ 433 | "e2448125.9980f" 434 | ] 435 | ] 436 | }, 437 | { 438 | "id": "814cf303.56d81", 439 | "type": "api-call-service", 440 | "z": "63d2914d.5ea9b", 441 | "name": "time of day: select option using context", 442 | "server": "4780c192.bc0f2", 443 | "service_domain": "input_select", 444 | "service": "select_option", 445 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 446 | "mergecontext": "timeOfDayOption", 447 | "x": 1020, 448 | "y": 660, 449 | "wires": [ 450 | [ 451 | "85b385c4.952628" 452 | ] 453 | ] 454 | }, 455 | { 456 | "id": "8b085e65.f230e", 457 | "type": "inject", 458 | "z": "63d2914d.5ea9b", 459 | "name": "run", 460 | "topic": "", 461 | "payload": "", 462 | "payloadType": "date", 463 | "repeat": "", 464 | "crontab": "", 465 | "once": false, 466 | "x": 110, 467 | "y": 660, 468 | "wires": [ 469 | [ 470 | "7816c484.1204ac" 471 | ] 472 | ] 473 | }, 474 | { 475 | "id": "c2e6bc7d.ebfbd", 476 | "type": "function", 477 | "z": "63d2914d.5ea9b", 478 | "name": "set GLOBAL ctx time of day 'Afternoon'", 479 | "func": "global.set('timeOfDayOption', {\n option: 'Afternoon'\n});\n\nreturn msg;", 480 | "outputs": 1, 481 | "noerr": 0, 482 | "x": 640, 483 | "y": 660, 484 | "wires": [ 485 | [ 486 | "814cf303.56d81" 487 | ] 488 | ] 489 | }, 490 | { 491 | "id": "7816c484.1204ac", 492 | "type": "function", 493 | "z": "63d2914d.5ea9b", 494 | "name": "clear flow and global ctx", 495 | "func": "flow.set('timeOfDayOption', null);\nglobal.set('timeOfDayOption', null);\n\nreturn msg;", 496 | "outputs": 1, 497 | "noerr": 0, 498 | "x": 310, 499 | "y": 660, 500 | "wires": [ 501 | [ 502 | "295b9ef1.721df2" 503 | ] 504 | ] 505 | }, 506 | { 507 | "id": "295b9ef1.721df2", 508 | "type": "function", 509 | "z": "63d2914d.5ea9b", 510 | "name": "set FLOW ctx time of day 'Morning'", 511 | "func": "flow.set('timeOfDayOption', {\noption: 'Morning'\n});\n\nreturn msg;", 512 | "outputs": 1, 513 | "noerr": 0, 514 | "x": 480, 515 | "y": 600, 516 | "wires": [ 517 | [ 518 | "c2e6bc7d.ebfbd" 519 | ] 520 | ] 521 | }, 522 | { 523 | "id": "117797c7.be7718", 524 | "type": "api-current-state", 525 | "z": "dc41607f.a08f2", 526 | "name": "Get Time of Day", 527 | "server": "4780c192.bc0f2", 528 | "halt_if": "", 529 | "override_topic": true, 530 | "override_payload": true, 531 | "entity_id": "input_select.time_of_day", 532 | "x": 360, 533 | "y": 120, 534 | "wires": [ 535 | [ 536 | "18ddeb10.8fa6d5" 537 | ] 538 | ] 539 | }, 540 | { 541 | "id": "5f649fcc.1b267", 542 | "type": "inject", 543 | "z": "dc41607f.a08f2", 544 | "name": "every 1 sec", 545 | "topic": "", 546 | "payload": "", 547 | "payloadType": "date", 548 | "repeat": "1", 549 | "crontab": "", 550 | "once": true, 551 | "x": 130, 552 | "y": 120, 553 | "wires": [ 554 | [ 555 | "117797c7.be7718" 556 | ] 557 | ] 558 | }, 559 | { 560 | "id": "18ddeb10.8fa6d5", 561 | "type": "debug", 562 | "z": "dc41607f.a08f2", 563 | "name": "", 564 | "active": true, 565 | "tosidebar": false, 566 | "console": false, 567 | "tostatus": true, 568 | "complete": "payload", 569 | "x": 610, 570 | "y": 120, 571 | "wires": [] 572 | }, 573 | { 574 | "id": "24372103.e2a25e", 575 | "type": "api-current-state", 576 | "z": "dc41607f.a08f2", 577 | "name": "Get Time of Day, Halt if 'Morning'", 578 | "server": "4780c192.bc0f2", 579 | "halt_if": "Morning", 580 | "override_topic": true, 581 | "override_payload": true, 582 | "entity_id": "input_select.time_of_day", 583 | "x": 380, 584 | "y": 260, 585 | "wires": [ 586 | [ 587 | "18c2e888.bab6c7" 588 | ] 589 | ] 590 | }, 591 | { 592 | "id": "18c2e888.bab6c7", 593 | "type": "debug", 594 | "z": "dc41607f.a08f2", 595 | "name": "", 596 | "active": true, 597 | "tosidebar": false, 598 | "console": false, 599 | "tostatus": true, 600 | "complete": "payload", 601 | "x": 630, 602 | "y": 260, 603 | "wires": [] 604 | }, 605 | { 606 | "id": "33d7626a.debd1e", 607 | "type": "api-get-history", 608 | "z": "ae2a9159.be871", 609 | "name": "Get History", 610 | "server": "4780c192.bc0f2", 611 | "startdate": "2018-05-02T20:00:00+00:00", 612 | "x": 290, 613 | "y": 80, 614 | "wires": [ 615 | [ 616 | "823a69f6.727d78" 617 | ] 618 | ] 619 | }, 620 | { 621 | "id": "9cf9aa59.bd5c98", 622 | "type": "inject", 623 | "z": "ae2a9159.be871", 624 | "name": "", 625 | "topic": "", 626 | "payload": "", 627 | "payloadType": "date", 628 | "repeat": "", 629 | "crontab": "", 630 | "once": false, 631 | "x": 140, 632 | "y": 80, 633 | "wires": [ 634 | [ 635 | "33d7626a.debd1e" 636 | ] 637 | ] 638 | }, 639 | { 640 | "id": "823a69f6.727d78", 641 | "type": "debug", 642 | "z": "ae2a9159.be871", 643 | "name": "", 644 | "active": true, 645 | "tosidebar": false, 646 | "console": false, 647 | "tostatus": true, 648 | "complete": "payload", 649 | "x": 450, 650 | "y": 80, 651 | "wires": [] 652 | }, 653 | { 654 | "id": "81862084.b5e3e", 655 | "type": "api-render-template", 656 | "z": "ae2a9159.be871", 657 | "name": "Get all Sensor States", 658 | "server": "4780c192.bc0f2", 659 | "template": "{% for state in states.sensor %}\n {{ state.entity_id }}={{ state.state }},\n{% endfor %}", 660 | "x": 360, 661 | "y": 140, 662 | "wires": [ 663 | [ 664 | "2b7e44e1.5ae46c" 665 | ] 666 | ] 667 | }, 668 | { 669 | "id": "b7401fa5.8bbe", 670 | "type": "inject", 671 | "z": "ae2a9159.be871", 672 | "name": "", 673 | "topic": "", 674 | "payload": "", 675 | "payloadType": "date", 676 | "repeat": "", 677 | "crontab": "", 678 | "once": false, 679 | "x": 140, 680 | "y": 140, 681 | "wires": [ 682 | [ 683 | "81862084.b5e3e" 684 | ] 685 | ] 686 | }, 687 | { 688 | "id": "2b7e44e1.5ae46c", 689 | "type": "debug", 690 | "z": "ae2a9159.be871", 691 | "name": "", 692 | "active": true, 693 | "tosidebar": true, 694 | "console": false, 695 | "tostatus": false, 696 | "complete": "payload", 697 | "x": 570, 698 | "y": 140, 699 | "wires": [] 700 | }, 701 | { 702 | "id": "8319fd04.fcf6f", 703 | "type": "poll-state", 704 | "z": "683ec059.246e5", 705 | "name": "poll state: sun.sun", 706 | "server": "4780c192.bc0f2", 707 | "updateinterval": "10", 708 | "outputinitially": false, 709 | "outputonchanged": false, 710 | "entity_id": "sun.sun", 711 | "x": 150, 712 | "y": 120, 713 | "wires": [ 714 | [ 715 | "dc144e5c.34425" 716 | ] 717 | ] 718 | }, 719 | { 720 | "id": "dc144e5c.34425", 721 | "type": "debug", 722 | "z": "683ec059.246e5", 723 | "name": "Last changed", 724 | "active": true, 725 | "tosidebar": false, 726 | "console": false, 727 | "tostatus": true, 728 | "complete": "payload.timeSinceChanged", 729 | "x": 380, 730 | "y": 120, 731 | "wires": [] 732 | }, 733 | { 734 | "id": "5bf86262.3ebf9c", 735 | "type": "poll-state", 736 | "z": "683ec059.246e5", 737 | "name": "poll state: input_select.time_of_day", 738 | "server": "4780c192.bc0f2", 739 | "updateinterval": "10", 740 | "outputinitially": true, 741 | "outputonchanged": true, 742 | "entity_id": "input_select.time_of_day", 743 | "x": 200, 744 | "y": 280, 745 | "wires": [ 746 | [ 747 | "c6b41d43.e2a53" 748 | ] 749 | ] 750 | }, 751 | { 752 | "id": "c6b41d43.e2a53", 753 | "type": "debug", 754 | "z": "683ec059.246e5", 755 | "name": "Last changed", 756 | "active": true, 757 | "tosidebar": false, 758 | "console": false, 759 | "tostatus": true, 760 | "complete": "payload.timeSinceChanged", 761 | "x": 480, 762 | "y": 280, 763 | "wires": [] 764 | }, 765 | { 766 | "id": "7330960e.a530b8", 767 | "type": "trigger-state", 768 | "z": "52eaaa74.1d85e4", 769 | "name": "Trigger: Input Select", 770 | "server": "4780c192.bc0f2", 771 | "entityid": "input_select.time_of_day", 772 | "debugenabled": false, 773 | "constraints": [], 774 | "constraintsmustmatch": "all", 775 | "outputs": 2, 776 | "customoutputs": [], 777 | "x": 220, 778 | "y": 140, 779 | "wires": [ 780 | [ 781 | "8026fff5.1fb7b" 782 | ], 783 | [ 784 | "6119a376.93111c" 785 | ] 786 | ] 787 | }, 788 | { 789 | "id": "1a5d748.4615b8c", 790 | "type": "comment", 791 | "z": "52eaaa74.1d85e4", 792 | "name": "default config behaves like state changed node, second ouput not used", 793 | "info": "", 794 | "x": 310, 795 | "y": 40, 796 | "wires": [] 797 | }, 798 | { 799 | "id": "8026fff5.1fb7b", 800 | "type": "debug", 801 | "z": "52eaaa74.1d85e4", 802 | "name": "passed constraints: state", 803 | "active": true, 804 | "tosidebar": false, 805 | "console": false, 806 | "tostatus": true, 807 | "complete": "payload", 808 | "x": 470, 809 | "y": 100, 810 | "wires": [] 811 | }, 812 | { 813 | "id": "f7f74b0e.ac88f8", 814 | "type": "inject", 815 | "z": "52eaaa74.1d85e4", 816 | "name": "select next option", 817 | "topic": "Select next option", 818 | "payload": "", 819 | "payloadType": "date", 820 | "repeat": "", 821 | "crontab": "", 822 | "once": false, 823 | "x": 890, 824 | "y": 780, 825 | "wires": [ 826 | [ 827 | "611a8b56.6ad224" 828 | ] 829 | ] 830 | }, 831 | { 832 | "id": "611a8b56.6ad224", 833 | "type": "api-call-service", 834 | "z": "52eaaa74.1d85e4", 835 | "name": "Time of Day: Select Next", 836 | "server": "4780c192.bc0f2", 837 | "service_domain": "input_select", 838 | "service": "select_next", 839 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 840 | "mergecontext": "", 841 | "x": 1130, 842 | "y": 820, 843 | "wires": [ 844 | [] 845 | ] 846 | }, 847 | { 848 | "id": "3e36e195.980d4e", 849 | "type": "server-state-changed", 850 | "z": "52eaaa74.1d85e4", 851 | "name": "on input_select.time_of_day", 852 | "server": "4780c192.bc0f2", 853 | "entityidfilter": "input_select.time_of_day", 854 | "entityidfiltertype": "substring", 855 | "haltifstate": "", 856 | "x": 1160, 857 | "y": 860, 858 | "wires": [ 859 | [ 860 | "272394ff.7a6a0c" 861 | ] 862 | ] 863 | }, 864 | { 865 | "id": "272394ff.7a6a0c", 866 | "type": "debug", 867 | "z": "52eaaa74.1d85e4", 868 | "name": "state", 869 | "active": true, 870 | "tosidebar": false, 871 | "console": false, 872 | "tostatus": true, 873 | "complete": "payload", 874 | "x": 1350, 875 | "y": 860, 876 | "wires": [] 877 | }, 878 | { 879 | "id": "f31f2d6c.6a9c7", 880 | "type": "trigger-state", 881 | "z": "52eaaa74.1d85e4", 882 | "name": "Trigger: Input Select", 883 | "server": "4780c192.bc0f2", 884 | "entityid": "input_select.time_of_day", 885 | "debugenabled": false, 886 | "constraints": [ 887 | { 888 | "id": "ho1v5jzhpm9", 889 | "targetType": "this_entity", 890 | "targetValue": "", 891 | "propertyType": "current_state", 892 | "propertyValue": "new_state.state", 893 | "comparatorType": "is", 894 | "comparatorValueDatatype": "str", 895 | "comparatorValue": "Afternoon" 896 | } 897 | ], 898 | "constraintsmustmatch": "all", 899 | "outputs": 2, 900 | "customoutputs": [], 901 | "x": 220, 902 | "y": 340, 903 | "wires": [ 904 | [ 905 | "ce44e49f.32b6e8" 906 | ], 907 | [ 908 | "5b4ae375.3318cc" 909 | ] 910 | ] 911 | }, 912 | { 913 | "id": "a02ea99f.77eab8", 914 | "type": "comment", 915 | "z": "52eaaa74.1d85e4", 916 | "name": "constraint: must be 'Afternoon'", 917 | "info": "", 918 | "x": 190, 919 | "y": 240, 920 | "wires": [] 921 | }, 922 | { 923 | "id": "6119a376.93111c", 924 | "type": "debug", 925 | "z": "52eaaa74.1d85e4", 926 | "name": "failed constraints: state", 927 | "active": true, 928 | "tosidebar": false, 929 | "console": false, 930 | "tostatus": true, 931 | "complete": "payload", 932 | "x": 470, 933 | "y": 180, 934 | "wires": [] 935 | }, 936 | { 937 | "id": "ce44e49f.32b6e8", 938 | "type": "debug", 939 | "z": "52eaaa74.1d85e4", 940 | "name": "passed constraints: state", 941 | "active": true, 942 | "tosidebar": false, 943 | "console": false, 944 | "tostatus": true, 945 | "complete": "payload", 946 | "x": 470, 947 | "y": 300, 948 | "wires": [] 949 | }, 950 | { 951 | "id": "5b4ae375.3318cc", 952 | "type": "debug", 953 | "z": "52eaaa74.1d85e4", 954 | "name": "failed constraints: state", 955 | "active": true, 956 | "tosidebar": false, 957 | "console": false, 958 | "tostatus": true, 959 | "complete": "payload", 960 | "x": 470, 961 | "y": 380, 962 | "wires": [] 963 | }, 964 | { 965 | "id": "fe97c29a.40634", 966 | "type": "comment", 967 | "z": "52eaaa74.1d85e4", 968 | "name": "use to test", 969 | "info": "", 970 | "x": 780, 971 | "y": 720, 972 | "wires": [] 973 | }, 974 | { 975 | "id": "1e17a4b3.a06b5b", 976 | "type": "trigger-state", 977 | "z": "52eaaa74.1d85e4", 978 | "name": "Trigger: Input Select", 979 | "server": "4780c192.bc0f2", 980 | "entityid": "input_select.time_of_day", 981 | "debugenabled": false, 982 | "constraints": [ 983 | { 984 | "id": "u8nfud48o9j", 985 | "targetType": "this_entity", 986 | "targetValue": "", 987 | "propertyType": "current_state", 988 | "propertyValue": "new_state.state", 989 | "comparatorType": "includes", 990 | "comparatorValueDatatype": "list", 991 | "comparatorValue": "Afternoon,Morning" 992 | } 993 | ], 994 | "constraintsmustmatch": "all", 995 | "outputs": 2, 996 | "customoutputs": [], 997 | "x": 220, 998 | "y": 580, 999 | "wires": [ 1000 | [ 1001 | "65cebebd.d5eb5" 1002 | ], 1003 | [ 1004 | "3937db25.d2c384" 1005 | ] 1006 | ] 1007 | }, 1008 | { 1009 | "id": "9e1dbdeb.f4cf2", 1010 | "type": "comment", 1011 | "z": "52eaaa74.1d85e4", 1012 | "name": "constraint: must include 'Afternoon,Morning'", 1013 | "info": "", 1014 | "x": 230, 1015 | "y": 480, 1016 | "wires": [] 1017 | }, 1018 | { 1019 | "id": "65cebebd.d5eb5", 1020 | "type": "debug", 1021 | "z": "52eaaa74.1d85e4", 1022 | "name": "passed constraints: state", 1023 | "active": true, 1024 | "tosidebar": false, 1025 | "console": false, 1026 | "tostatus": true, 1027 | "complete": "payload", 1028 | "x": 470, 1029 | "y": 540, 1030 | "wires": [] 1031 | }, 1032 | { 1033 | "id": "3937db25.d2c384", 1034 | "type": "debug", 1035 | "z": "52eaaa74.1d85e4", 1036 | "name": "failed constraints: state", 1037 | "active": true, 1038 | "tosidebar": false, 1039 | "console": false, 1040 | "tostatus": true, 1041 | "complete": "payload", 1042 | "x": 470, 1043 | "y": 620, 1044 | "wires": [] 1045 | }, 1046 | { 1047 | "id": "6bcc3557.1579ec", 1048 | "type": "trigger-state", 1049 | "z": "52eaaa74.1d85e4", 1050 | "name": "Trigger: Input Select", 1051 | "server": "4780c192.bc0f2", 1052 | "entityid": "input_select.time_of_day", 1053 | "debugenabled": false, 1054 | "constraints": [ 1055 | { 1056 | "id": "yubzwdpovxb", 1057 | "targetType": "this_entity", 1058 | "targetValue": "", 1059 | "propertyType": "current_state", 1060 | "propertyValue": "new_state.state", 1061 | "comparatorType": "is_not", 1062 | "comparatorValueDatatype": "str", 1063 | "comparatorValue": "Party Time" 1064 | } 1065 | ], 1066 | "constraintsmustmatch": "all", 1067 | "outputs": 2, 1068 | "customoutputs": [], 1069 | "x": 220, 1070 | "y": 800, 1071 | "wires": [ 1072 | [ 1073 | "fa7a230d.cf9fd" 1074 | ], 1075 | [ 1076 | "1991a7eb.ecffd8" 1077 | ] 1078 | ] 1079 | }, 1080 | { 1081 | "id": "da616990.c45f58", 1082 | "type": "comment", 1083 | "z": "52eaaa74.1d85e4", 1084 | "name": "constraint: is not 'Party Time'", 1085 | "info": "", 1086 | "x": 180, 1087 | "y": 700, 1088 | "wires": [] 1089 | }, 1090 | { 1091 | "id": "fa7a230d.cf9fd", 1092 | "type": "debug", 1093 | "z": "52eaaa74.1d85e4", 1094 | "name": "passed constraints: state", 1095 | "active": true, 1096 | "tosidebar": false, 1097 | "console": false, 1098 | "tostatus": true, 1099 | "complete": "payload", 1100 | "x": 470, 1101 | "y": 760, 1102 | "wires": [] 1103 | }, 1104 | { 1105 | "id": "1991a7eb.ecffd8", 1106 | "type": "debug", 1107 | "z": "52eaaa74.1d85e4", 1108 | "name": "failed constraints: state", 1109 | "active": true, 1110 | "tosidebar": false, 1111 | "console": false, 1112 | "tostatus": true, 1113 | "complete": "payload", 1114 | "x": 470, 1115 | "y": 840, 1116 | "wires": [] 1117 | }, 1118 | { 1119 | "id": "a47272f4.665a2", 1120 | "type": "trigger-state", 1121 | "z": "52eaaa74.1d85e4", 1122 | "name": "Trigger: Input Select", 1123 | "server": "4780c192.bc0f2", 1124 | "entityid": "input_select.time_of_day", 1125 | "debugenabled": false, 1126 | "constraints": [ 1127 | { 1128 | "id": "yubzwdpovxb", 1129 | "targetType": "this_entity", 1130 | "targetValue": "", 1131 | "propertyType": "current_state", 1132 | "propertyValue": "new_state.state", 1133 | "comparatorType": "is_not", 1134 | "comparatorValueDatatype": "str", 1135 | "comparatorValue": "Party Time" 1136 | } 1137 | ], 1138 | "constraintsmustmatch": "all", 1139 | "outputs": 4, 1140 | "customoutputs": [ 1141 | { 1142 | "outputId": "juuw05k2nsa", 1143 | "messageType": "default", 1144 | "messageValue": "", 1145 | "comparatorPropertyType": "current_state", 1146 | "comparatorPropertyValue": "new_state.state", 1147 | "comparatorType": "is", 1148 | "comparatorValue": "Morning" 1149 | }, 1150 | { 1151 | "outputId": "kd1jwe5mefe", 1152 | "messageType": "default", 1153 | "messageValue": "", 1154 | "comparatorPropertyType": "current_state", 1155 | "comparatorPropertyValue": "new_state.state", 1156 | "comparatorType": "is", 1157 | "comparatorValue": "Afternoon" 1158 | } 1159 | ], 1160 | "x": 880, 1161 | "y": 200, 1162 | "wires": [ 1163 | [ 1164 | "f4085e9f.a6179" 1165 | ], 1166 | [ 1167 | "600b0c2b.7f6404" 1168 | ], 1169 | [ 1170 | "f43d545f.504f98" 1171 | ], 1172 | [ 1173 | "543ad6dc.d0e148" 1174 | ] 1175 | ] 1176 | }, 1177 | { 1178 | "id": "f4085e9f.a6179", 1179 | "type": "debug", 1180 | "z": "52eaaa74.1d85e4", 1181 | "name": "passed constraints: state", 1182 | "active": true, 1183 | "tosidebar": false, 1184 | "console": false, 1185 | "tostatus": true, 1186 | "complete": "payload", 1187 | "x": 1150, 1188 | "y": 100, 1189 | "wires": [] 1190 | }, 1191 | { 1192 | "id": "600b0c2b.7f6404", 1193 | "type": "debug", 1194 | "z": "52eaaa74.1d85e4", 1195 | "name": "failed constraints: state", 1196 | "active": true, 1197 | "tosidebar": false, 1198 | "console": false, 1199 | "tostatus": true, 1200 | "complete": "payload", 1201 | "x": 1150, 1202 | "y": 160, 1203 | "wires": [] 1204 | }, 1205 | { 1206 | "id": "85bd242b.923558", 1207 | "type": "comment", 1208 | "z": "52eaaa74.1d85e4", 1209 | "name": "constraint: is not 'Party Time' with 2 outputs for 'Morning' and 'Afternoon' ", 1210 | "info": "", 1211 | "x": 1020, 1212 | "y": 40, 1213 | "wires": [] 1214 | }, 1215 | { 1216 | "id": "f43d545f.504f98", 1217 | "type": "debug", 1218 | "z": "52eaaa74.1d85e4", 1219 | "name": "state is 'Morning'", 1220 | "active": true, 1221 | "tosidebar": false, 1222 | "console": false, 1223 | "tostatus": true, 1224 | "complete": "payload", 1225 | "x": 1130, 1226 | "y": 240, 1227 | "wires": [] 1228 | }, 1229 | { 1230 | "id": "543ad6dc.d0e148", 1231 | "type": "debug", 1232 | "z": "52eaaa74.1d85e4", 1233 | "name": "state is 'Afternoon'", 1234 | "active": true, 1235 | "tosidebar": false, 1236 | "console": false, 1237 | "tostatus": true, 1238 | "complete": "payload", 1239 | "x": 1130, 1240 | "y": 300, 1241 | "wires": [] 1242 | }, 1243 | { 1244 | "id": "be0ee0bf.d6264", 1245 | "type": "inject", 1246 | "z": "52eaaa74.1d85e4", 1247 | "name": "Set to 'Morning'", 1248 | "topic": "", 1249 | "payload": "{\"service\":\"select_option\",\"data\":{\"option\":\"Morning\"}}", 1250 | "payloadType": "json", 1251 | "repeat": "", 1252 | "crontab": "", 1253 | "once": false, 1254 | "x": 880, 1255 | "y": 820, 1256 | "wires": [ 1257 | [ 1258 | "611a8b56.6ad224" 1259 | ] 1260 | ] 1261 | }, 1262 | { 1263 | "id": "48645608.c8b738", 1264 | "type": "inject", 1265 | "z": "52eaaa74.1d85e4", 1266 | "name": "Set to 'Party Time'", 1267 | "topic": "", 1268 | "payload": "{\"service\":\"select_option\",\"data\":{\"option\":\"Party Time\"}}", 1269 | "payloadType": "json", 1270 | "repeat": "", 1271 | "crontab": "", 1272 | "once": false, 1273 | "x": 890, 1274 | "y": 860, 1275 | "wires": [ 1276 | [ 1277 | "611a8b56.6ad224" 1278 | ] 1279 | ] 1280 | }, 1281 | { 1282 | "id": "ac95cd16.a203f", 1283 | "type": "trigger-state", 1284 | "z": "52eaaa74.1d85e4", 1285 | "name": "Trigger: Input Select", 1286 | "server": "4780c192.bc0f2", 1287 | "entityid": "input_select.time_of_day", 1288 | "debugenabled": false, 1289 | "constraints": [ 1290 | { 1291 | "id": "tdgnzb9d3pc", 1292 | "targetType": "entity_id", 1293 | "targetValue": "sun.sun", 1294 | "propertyType": "current_state", 1295 | "propertyValue": "new_state.state", 1296 | "comparatorType": "is", 1297 | "comparatorValueDatatype": "str", 1298 | "comparatorValue": "below_horizon" 1299 | }, 1300 | { 1301 | "id": "2ntp5gceqn8", 1302 | "targetType": "this_entity", 1303 | "targetValue": "", 1304 | "propertyType": "current_state", 1305 | "propertyValue": "new_state.state", 1306 | "comparatorType": "is", 1307 | "comparatorValueDatatype": "str", 1308 | "comparatorValue": "Party Time" 1309 | } 1310 | ], 1311 | "constraintsmustmatch": "all", 1312 | "outputs": 2, 1313 | "customoutputs": [], 1314 | "x": 940, 1315 | "y": 480, 1316 | "wires": [ 1317 | [ 1318 | "23d2b566.b865ca" 1319 | ], 1320 | [ 1321 | "1a0a5e08.82b5b2" 1322 | ] 1323 | ] 1324 | }, 1325 | { 1326 | "id": "18e05aa6.2ec855", 1327 | "type": "comment", 1328 | "z": "52eaaa74.1d85e4", 1329 | "name": "constraint: is 'Party Time' and sun.sun is 'below_horizon'", 1330 | "info": "", 1331 | "x": 990, 1332 | "y": 380, 1333 | "wires": [] 1334 | }, 1335 | { 1336 | "id": "23d2b566.b865ca", 1337 | "type": "debug", 1338 | "z": "52eaaa74.1d85e4", 1339 | "name": "passed constraints: state", 1340 | "active": true, 1341 | "tosidebar": false, 1342 | "console": false, 1343 | "tostatus": true, 1344 | "complete": "payload", 1345 | "x": 1190, 1346 | "y": 440, 1347 | "wires": [] 1348 | }, 1349 | { 1350 | "id": "1a0a5e08.82b5b2", 1351 | "type": "debug", 1352 | "z": "52eaaa74.1d85e4", 1353 | "name": "failed constraints: state", 1354 | "active": true, 1355 | "tosidebar": false, 1356 | "console": false, 1357 | "tostatus": true, 1358 | "complete": "payload", 1359 | "x": 1190, 1360 | "y": 520, 1361 | "wires": [] 1362 | }, 1363 | { 1364 | "id": "64a92f1d.cf9ab", 1365 | "type": "server-state-changed", 1366 | "z": "52eaaa74.1d85e4", 1367 | "name": "on sun.sun state", 1368 | "server": "4780c192.bc0f2", 1369 | "entityidfilter": "sun.sun", 1370 | "entityidfiltertype": "substring", 1371 | "haltifstate": "", 1372 | "x": 1120, 1373 | "y": 920, 1374 | "wires": [ 1375 | [ 1376 | "25b2ddb0.046292" 1377 | ] 1378 | ] 1379 | }, 1380 | { 1381 | "id": "25b2ddb0.046292", 1382 | "type": "debug", 1383 | "z": "52eaaa74.1d85e4", 1384 | "name": "state", 1385 | "active": true, 1386 | "tosidebar": false, 1387 | "console": false, 1388 | "tostatus": true, 1389 | "complete": "payload", 1390 | "x": 1350, 1391 | "y": 920, 1392 | "wires": [] 1393 | }, 1394 | { 1395 | "id": "80b2b91a.661468", 1396 | "type": "comment", 1397 | "z": "63d2914d.5ea9b", 1398 | "name": "Global and Flow contexts can be used to override / merge call service data", 1399 | "info": "", 1400 | "x": 300, 1401 | "y": 340, 1402 | "wires": [] 1403 | }, 1404 | { 1405 | "id": "38093beb.48d9c4", 1406 | "type": "comment", 1407 | "z": "63d2914d.5ea9b", 1408 | "name": "If both Flow and Global contexts are set for the same property the FLOW context will win", 1409 | "info": "", 1410 | "x": 350, 1411 | "y": 540, 1412 | "wires": [] 1413 | }, 1414 | { 1415 | "id": "2477d4ab.9d4aac", 1416 | "type": "comment", 1417 | "z": "63d2914d.5ea9b", 1418 | "name": "Call service can be configured in the node itself and allows overrides via payload", 1419 | "info": "", 1420 | "x": 300, 1421 | "y": 40, 1422 | "wires": [] 1423 | }, 1424 | { 1425 | "id": "c7aca58d.8ed7f8", 1426 | "type": "inject", 1427 | "z": "dc41607f.a08f2", 1428 | "name": "select next option", 1429 | "topic": "Select next option", 1430 | "payload": "", 1431 | "payloadType": "date", 1432 | "repeat": "", 1433 | "crontab": "", 1434 | "once": false, 1435 | "x": 1030, 1436 | "y": 100, 1437 | "wires": [ 1438 | [ 1439 | "217e09d9.1ab0d6" 1440 | ] 1441 | ] 1442 | }, 1443 | { 1444 | "id": "217e09d9.1ab0d6", 1445 | "type": "api-call-service", 1446 | "z": "dc41607f.a08f2", 1447 | "name": "Time of Day: Select Next", 1448 | "server": "4780c192.bc0f2", 1449 | "service_domain": "input_select", 1450 | "service": "select_next", 1451 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 1452 | "mergecontext": "", 1453 | "x": 1270, 1454 | "y": 100, 1455 | "wires": [ 1456 | [] 1457 | ] 1458 | }, 1459 | { 1460 | "id": "c23e8d1d.7ad4c", 1461 | "type": "comment", 1462 | "z": "dc41607f.a08f2", 1463 | "name": "use to test", 1464 | "info": "", 1465 | "x": 940, 1466 | "y": 60, 1467 | "wires": [] 1468 | }, 1469 | { 1470 | "id": "6b1830df.9bbeb", 1471 | "type": "comment", 1472 | "z": "dc41607f.a08f2", 1473 | "name": "fetch state of entity mid flow", 1474 | "info": "", 1475 | "x": 140, 1476 | "y": 60, 1477 | "wires": [] 1478 | }, 1479 | { 1480 | "id": "e56cb26e.4430e", 1481 | "type": "comment", 1482 | "z": "dc41607f.a08f2", 1483 | "name": "fetch state, halt if equals 'Morning'", 1484 | "info": "", 1485 | "x": 160, 1486 | "y": 200, 1487 | "wires": [] 1488 | }, 1489 | { 1490 | "id": "f7784f84.9be3a", 1491 | "type": "inject", 1492 | "z": "dc41607f.a08f2", 1493 | "name": "every 1 sec", 1494 | "topic": "", 1495 | "payload": "", 1496 | "payloadType": "date", 1497 | "repeat": "1", 1498 | "crontab": "", 1499 | "once": true, 1500 | "x": 130, 1501 | "y": 260, 1502 | "wires": [ 1503 | [ 1504 | "24372103.e2a25e" 1505 | ] 1506 | ] 1507 | }, 1508 | { 1509 | "id": "a5db542a.600ac8", 1510 | "type": "comment", 1511 | "z": "683ec059.246e5", 1512 | "name": "poll sun state every 10 seconds", 1513 | "info": "", 1514 | "x": 150, 1515 | "y": 60, 1516 | "wires": [] 1517 | }, 1518 | { 1519 | "id": "319cdd28.b17922", 1520 | "type": "comment", 1521 | "z": "683ec059.246e5", 1522 | "name": "poll every 10 sec, once at start, and when state changes", 1523 | "info": "", 1524 | "x": 230, 1525 | "y": 220, 1526 | "wires": [] 1527 | }, 1528 | { 1529 | "id": "b4bfc915.5f9078", 1530 | "type": "inject", 1531 | "z": "683ec059.246e5", 1532 | "name": "select next option", 1533 | "topic": "Select next option", 1534 | "payload": "", 1535 | "payloadType": "date", 1536 | "repeat": "", 1537 | "crontab": "", 1538 | "once": false, 1539 | "x": 910, 1540 | "y": 120, 1541 | "wires": [ 1542 | [ 1543 | "cf4589db.e66398" 1544 | ] 1545 | ] 1546 | }, 1547 | { 1548 | "id": "cf4589db.e66398", 1549 | "type": "api-call-service", 1550 | "z": "683ec059.246e5", 1551 | "name": "Time of Day: Select Next", 1552 | "server": "4780c192.bc0f2", 1553 | "service_domain": "input_select", 1554 | "service": "select_next", 1555 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 1556 | "mergecontext": "", 1557 | "x": 1150, 1558 | "y": 120, 1559 | "wires": [ 1560 | [] 1561 | ] 1562 | }, 1563 | { 1564 | "id": "ebc396a4.1ca738", 1565 | "type": "comment", 1566 | "z": "683ec059.246e5", 1567 | "name": "use to test", 1568 | "info": "", 1569 | "x": 820, 1570 | "y": 80, 1571 | "wires": [] 1572 | }, 1573 | { 1574 | "id": "ea9a4cf1.38742", 1575 | "type": "comment", 1576 | "z": "c81c02fc.f75a5", 1577 | "name": "Get state of an entity from function node", 1578 | "info": "", 1579 | "x": 520, 1580 | "y": 140, 1581 | "wires": [] 1582 | }, 1583 | { 1584 | "id": "e9d3911a.f262a", 1585 | "type": "comment", 1586 | "z": "c81c02fc.f75a5", 1587 | "name": "Get multiple states", 1588 | "info": "", 1589 | "x": 450, 1590 | "y": 280, 1591 | "wires": [] 1592 | }, 1593 | { 1594 | "id": "cb30508a.c6d78", 1595 | "type": "function", 1596 | "z": "c81c02fc.f75a5", 1597 | "name": "return time_of_day input state", 1598 | "func": "const haCtx = global.get('homeassistant').homeAssistant;\n\nconst getState = (e) => haCtx.states[e];\n\nconst inputState = getState('input_select.time_of_day');\nconst sunState = getState('sun.sun');\n\nreturn [ \n { payload: inputState.state },\n { payload: sunState.state }\n];", 1599 | "outputs": 2, 1600 | "noerr": 0, 1601 | "x": 550, 1602 | "y": 320, 1603 | "wires": [ 1604 | [ 1605 | "c18f04f0.0d2748" 1606 | ], 1607 | [ 1608 | "4c144265.edca2c" 1609 | ] 1610 | ], 1611 | "outputLabels": [ 1612 | "input state", 1613 | "sun state" 1614 | ] 1615 | }, 1616 | { 1617 | "id": "c18f04f0.0d2748", 1618 | "type": "debug", 1619 | "z": "c81c02fc.f75a5", 1620 | "name": "input state", 1621 | "active": true, 1622 | "tosidebar": false, 1623 | "console": false, 1624 | "tostatus": true, 1625 | "complete": "payload", 1626 | "x": 810, 1627 | "y": 300, 1628 | "wires": [] 1629 | }, 1630 | { 1631 | "id": "4c144265.edca2c", 1632 | "type": "debug", 1633 | "z": "c81c02fc.f75a5", 1634 | "name": "sun state", 1635 | "active": true, 1636 | "tosidebar": false, 1637 | "console": false, 1638 | "tostatus": true, 1639 | "complete": "payload", 1640 | "x": 800, 1641 | "y": 360, 1642 | "wires": [] 1643 | }, 1644 | { 1645 | "id": "67539b12.e9ec74", 1646 | "type": "inject", 1647 | "z": "c81c02fc.f75a5", 1648 | "name": "", 1649 | "topic": "", 1650 | "payload": "", 1651 | "payloadType": "date", 1652 | "repeat": "", 1653 | "crontab": "", 1654 | "once": false, 1655 | "onceDelay": 0.1, 1656 | "x": 200, 1657 | "y": 320, 1658 | "wires": [ 1659 | [ 1660 | "d7d4d181.51b4", 1661 | "cb30508a.c6d78", 1662 | "c3a058eb.7104b8" 1663 | ] 1664 | ] 1665 | }, 1666 | { 1667 | "id": "d7d4d181.51b4", 1668 | "type": "function", 1669 | "z": "c81c02fc.f75a5", 1670 | "name": "return time_of_day input state", 1671 | "func": "const haCtx = global.get('homeassistant').homeAssistant;\n\nconst inputState = haCtx.states['input_select.time_of_day'];\n\nmsg.payload = inputState.state;\nreturn msg;", 1672 | "outputs": 1, 1673 | "noerr": 0, 1674 | "x": 550, 1675 | "y": 200, 1676 | "wires": [ 1677 | [ 1678 | "36dac7c9.baac58" 1679 | ] 1680 | ] 1681 | }, 1682 | { 1683 | "id": "36dac7c9.baac58", 1684 | "type": "debug", 1685 | "z": "c81c02fc.f75a5", 1686 | "name": "", 1687 | "active": true, 1688 | "tosidebar": false, 1689 | "console": false, 1690 | "tostatus": true, 1691 | "complete": "payload", 1692 | "x": 790, 1693 | "y": 200, 1694 | "wires": [] 1695 | }, 1696 | { 1697 | "id": "f6e08c9b.fa84a", 1698 | "type": "comment", 1699 | "z": "c81c02fc.f75a5", 1700 | "name": "Ouput once per entity list", 1701 | "info": "", 1702 | "x": 470, 1703 | "y": 420, 1704 | "wires": [] 1705 | }, 1706 | { 1707 | "id": "c3a058eb.7104b8", 1708 | "type": "function", 1709 | "z": "c81c02fc.f75a5", 1710 | "name": "return time_of_day input state", 1711 | "func": "const haCtx = global.get('homeassistant').homeAssistant;\n\nconst interestingEntities = [\n 'input_select.time_of_day',\n 'sun.sun'\n];\n\nconst states = interestingEntities\n .map(eid => haCtx.states[eid]);\n\nreturn states;", 1712 | "outputs": 1, 1713 | "noerr": 0, 1714 | "x": 550, 1715 | "y": 480, 1716 | "wires": [ 1717 | [ 1718 | "3e5e6413.4cd8fc" 1719 | ] 1720 | ] 1721 | }, 1722 | { 1723 | "id": "3e5e6413.4cd8fc", 1724 | "type": "switch", 1725 | "z": "c81c02fc.f75a5", 1726 | "name": "", 1727 | "property": "entity_id", 1728 | "propertyType": "msg", 1729 | "rules": [ 1730 | { 1731 | "t": "eq", 1732 | "v": "input_select.time_of_day", 1733 | "vt": "str" 1734 | }, 1735 | { 1736 | "t": "eq", 1737 | "v": "sun.sun", 1738 | "vt": "str" 1739 | } 1740 | ], 1741 | "checkall": "true", 1742 | "repair": false, 1743 | "outputs": 2, 1744 | "x": 770, 1745 | "y": 480, 1746 | "wires": [ 1747 | [ 1748 | "e0b3e2c2.87014" 1749 | ], 1750 | [ 1751 | "b3083555.1cf108" 1752 | ] 1753 | ] 1754 | }, 1755 | { 1756 | "id": "e0b3e2c2.87014", 1757 | "type": "debug", 1758 | "z": "c81c02fc.f75a5", 1759 | "name": "input state", 1760 | "active": true, 1761 | "tosidebar": false, 1762 | "console": false, 1763 | "tostatus": true, 1764 | "complete": "state", 1765 | "x": 970, 1766 | "y": 440, 1767 | "wires": [] 1768 | }, 1769 | { 1770 | "id": "b3083555.1cf108", 1771 | "type": "debug", 1772 | "z": "c81c02fc.f75a5", 1773 | "name": "sun state", 1774 | "active": true, 1775 | "tosidebar": false, 1776 | "console": false, 1777 | "tostatus": true, 1778 | "complete": "state", 1779 | "x": 960, 1780 | "y": 520, 1781 | "wires": [] 1782 | }, 1783 | { 1784 | "id": "ea2f46e7.871258", 1785 | "type": "function", 1786 | "z": "63d2914d.5ea9b", 1787 | "name": "[TEST] API payload data is correct", 1788 | "func": "const testStatus = msg.testStatus = { passed: true, text: 'Test Passed', datetime: new Date(), shape: 'dot', fill: 'green' };\n\nconst { domain, service, data } = msg.payload;\n\n// FIXME: entity_id is getting an extra space at start from somewhere? API call still works but this should be cleaned up\nif (domain !== 'input_select' || service !== 'select_next' || !data || data.entity_id !== ' input_select.time_of_day' ) {\n testStatus.passed = false; \n testStatus.text = 'Test Failed';\n testStatus.fill = 'red';\n}\n\nnode.status(testStatus);\nreturn msg;\n", 1789 | "outputs": 1, 1790 | "noerr": 0, 1791 | "x": 820, 1792 | "y": 100, 1793 | "wires": [ 1794 | [ 1795 | "a5ae821e.65219" 1796 | ] 1797 | ] 1798 | }, 1799 | { 1800 | "id": "9175adc1.7cf5c", 1801 | "type": "function", 1802 | "z": "63d2914d.5ea9b", 1803 | "name": "[TEST] state === 'Afternoon'", 1804 | "func": "// Test that state is actually changed withing home assistant\n\nsetTimeout(() => {\n const haCtx = global.get('homeassistant').homeAssistant;\n const inputState = haCtx.states['input_select.time_of_day'];\n\n if (inputState.state !== 'Afternoon') {\n const text = 'Test Failed: Expected \"Afternoon\"';\n node.status({ shape: 'dot', fill: 'red', text })\n throw new Error(text);\n } else {\n const text = 'Test Passed';\n node.status({ shape: 'dot', fill: 'green', text })\n node.send(msg);\n }\n}, 100);\n\nreturn;\n", 1805 | "outputs": 1, 1806 | "noerr": 0, 1807 | "x": 1280, 1808 | "y": 380, 1809 | "wires": [ 1810 | [] 1811 | ] 1812 | }, 1813 | { 1814 | "id": "2517417.5e2073e", 1815 | "type": "api-call-service", 1816 | "z": "63d2914d.5ea9b", 1817 | "name": "time of day: select option using context", 1818 | "server": "4780c192.bc0f2", 1819 | "service_domain": "input_select", 1820 | "service": "select_option", 1821 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 1822 | "mergecontext": "timeOfDayOption", 1823 | "x": 960, 1824 | "y": 440, 1825 | "wires": [ 1826 | [ 1827 | "978dccc.f7e38b" 1828 | ] 1829 | ] 1830 | }, 1831 | { 1832 | "id": "978dccc.f7e38b", 1833 | "type": "function", 1834 | "z": "63d2914d.5ea9b", 1835 | "name": "[TEST] state === 'Morning'", 1836 | "func": "// Test that state is actually changed withing home assistant\n\nsetTimeout(() => {\n const haCtx = global.get('homeassistant').homeAssistant;\n const inputState = haCtx.states['input_select.time_of_day'];\n\n if (inputState.state !== 'Morning') {\n const text = 'Test Failed: Expected \"Afternoon\"';\n node.status({ shape: 'dot', fill: 'red', text })\n throw new Error(text);\n } else {\n const text = 'Test Passed';\n node.status({ shape: 'dot', fill: 'green', text })\n node.send(msg);\n }\n}, 100);\n\nreturn;\n", 1837 | "outputs": 1, 1838 | "noerr": 0, 1839 | "x": 1280, 1840 | "y": 460, 1841 | "wires": [ 1842 | [] 1843 | ] 1844 | }, 1845 | { 1846 | "id": "85b385c4.952628", 1847 | "type": "function", 1848 | "z": "63d2914d.5ea9b", 1849 | "name": "[TEST] state === 'Morning'", 1850 | "func": "// Test that state is actually changed withing home assistant\n\nsetTimeout(() => {\n const haCtx = global.get('homeassistant').homeAssistant;\n const inputState = haCtx.states['input_select.time_of_day'];\n\n if (inputState.state !== 'Morning') {\n const text = 'Test Failed: Expected \"Afternoon\"';\n node.status({ shape: 'dot', fill: 'red', text })\n throw new Error(text);\n } else {\n const text = 'Test Passed';\n node.status({ shape: 'dot', fill: 'green', text })\n node.send(msg);\n }\n}, 100);\n\nreturn;\n", 1851 | "outputs": 1, 1852 | "noerr": 0, 1853 | "x": 1360, 1854 | "y": 660, 1855 | "wires": [ 1856 | [] 1857 | ] 1858 | }, 1859 | { 1860 | "id": "a5ae821e.65219", 1861 | "type": "debug", 1862 | "z": "63d2914d.5ea9b", 1863 | "name": "log result", 1864 | "active": true, 1865 | "tosidebar": true, 1866 | "console": false, 1867 | "tostatus": true, 1868 | "complete": "true", 1869 | "x": 1120, 1870 | "y": 160, 1871 | "wires": [] 1872 | }, 1873 | { 1874 | "id": "eba49a.66b6cb68", 1875 | "type": "api-call-service", 1876 | "z": "63d2914d.5ea9b", 1877 | "name": "Time of Day: Select Next", 1878 | "server": "4780c192.bc0f2", 1879 | "service_domain": "input_select", 1880 | "service": "select_next", 1881 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 1882 | "mergecontext": "", 1883 | "x": 530, 1884 | "y": 160, 1885 | "wires": [ 1886 | [ 1887 | "4b8fe84d.98e11" 1888 | ] 1889 | ] 1890 | }, 1891 | { 1892 | "id": "4b8fe84d.98e11", 1893 | "type": "function", 1894 | "z": "63d2914d.5ea9b", 1895 | "name": "[TEST] API payload data is correct", 1896 | "func": "const testStatus = msg.testStatus = { passed: true, text: 'Test Passed', datetime: new Date(), shape: 'dot', fill: 'green' };\nconst { domain, service, data } = msg.payload;\n\n// FIXME: entity_id is getting an extra space at start from somewhere? API call still works but this should be cleaned up\nif (domain !== 'input_select' || service !== 'select_previous' || !data || data.entity_id !== ' input_select.time_of_day' ) {\n testStatus.passed = false; \n testStatus.text = 'Test Failed';\n testStatus.fill = 'red';\n}\n\nnode.status(testStatus);\nreturn msg;\n", 1897 | "outputs": 1, 1898 | "noerr": 0, 1899 | "x": 820, 1900 | "y": 160, 1901 | "wires": [ 1902 | [ 1903 | "a5ae821e.65219" 1904 | ] 1905 | ] 1906 | }, 1907 | { 1908 | "id": "f59a907f.7f08b8", 1909 | "type": "api-call-service", 1910 | "z": "63d2914d.5ea9b", 1911 | "name": "Time of Day: Select Next", 1912 | "server": "4780c192.bc0f2", 1913 | "service_domain": "input_select", 1914 | "service": "select_next", 1915 | "data": "{ \"entity_id\": \" input_select.time_of_day\" }", 1916 | "mergecontext": "", 1917 | "x": 530, 1918 | "y": 220, 1919 | "wires": [ 1920 | [ 1921 | "d3e9c8b5.ea3958" 1922 | ] 1923 | ] 1924 | }, 1925 | { 1926 | "id": "d3e9c8b5.ea3958", 1927 | "type": "function", 1928 | "z": "63d2914d.5ea9b", 1929 | "name": "[TEST] API payload data is correct", 1930 | "func": "const testStatus = msg.testStatus = { passed: true, text: 'Test Passed', datetime: new Date(), shape: 'dot', fill: 'green' };\n\nconst { domain, service, data } = msg.payload;\n\n// FIXME: entity_id is getting an extra space at start from somewhere? API call still works but this should be cleaned up\nif (domain !== 'input_select' || service !== 'select_option' || !data || data.entity_id !== ' input_select.time_of_day' || data.option !== 'Party Time' ) {\n testStatus.passed = false; \n testStatus.text = 'Test Failed';\n testStatus.fill = 'red';\n}\n\nnode.status(testStatus);\nreturn msg;\n", 1931 | "outputs": 1, 1932 | "noerr": 0, 1933 | "x": 820, 1934 | "y": 220, 1935 | "wires": [ 1936 | [ 1937 | "a5ae821e.65219" 1938 | ] 1939 | ] 1940 | } 1941 | ] -------------------------------------------------------------------------------- /docker/node-red/root-fs/data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-project", 3 | "description": "A Node-RED Project", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "start": "node red.js", 7 | "dev:watch": "nodemon --config /data-dev/nodemon.json --inspect /usr/src/node-red/node_modules/node-red/red.js -- -v --userDir /data-dev flows.json", 8 | "test": "grunt", 9 | "build": "grunt build" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/base-node.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const merge = require('lodash.merge'); 3 | const selectn = require('selectn'); 4 | const dateFns = require('date-fns'); 5 | 6 | const low = require('lowdb'); 7 | const FileAsync = require('lowdb/adapters/FileAsync'); 8 | 9 | const utils = { 10 | selectn, 11 | merge, 12 | Joi, 13 | reach: (path, obj) => selectn(path, obj), 14 | formatDate: (date) => dateFns.format(date, 'ddd, h:mm:ss A'), 15 | toCamelCase(str) { 16 | return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) { 17 | if (+match === 0) return ''; 18 | return index == 0 ? match.toLowerCase() : match.toUpperCase(); // eslint-disable-line 19 | }); 20 | } 21 | }; 22 | 23 | const DEFAULT_OPTIONS = { 24 | config: { 25 | debugenabled: {}, 26 | name: {}, 27 | server: { isNode: true } 28 | }, 29 | input: { 30 | topic: { messageProp: 'topic' }, 31 | payload: { messageProp: 'payload' } 32 | } 33 | }; 34 | 35 | let DB; 36 | 37 | class BaseNode { 38 | constructor(nodeDefinition, RED, options = {}) { 39 | // Need to bring in NodeRed dependency and properly extend Node class, or just make this class a node handler 40 | RED.nodes.createNode(this, nodeDefinition); 41 | this.node = this; 42 | 43 | this.RED = RED; 44 | this.options = merge({}, DEFAULT_OPTIONS, options); 45 | this._eventHandlers = _eventHandlers; 46 | this._internals = _internals; 47 | this.utils = utils; 48 | 49 | this.nodeConfig = Object.entries(this.options.config).reduce((acc, [key, config]) => { 50 | if (config.isNode) { 51 | acc[key] = this.RED.nodes.getNode(nodeDefinition[key]); 52 | } else if (typeof config === 'function') { 53 | acc[key] = config.call(this, nodeDefinition); 54 | } else { 55 | acc[key] = nodeDefinition[key]; 56 | } 57 | 58 | return acc; 59 | }, {}); 60 | 61 | this.node.on('input', this._eventHandlers.preOnInput.bind(this)); 62 | this.node.on('close', this._eventHandlers.preOnClose.bind(this)); 63 | 64 | const name = this.reach('nodeConfig.name'); 65 | this.debug(`instantiated node, name: ${name || 'undefined'}`); 66 | } 67 | 68 | async getDb() { 69 | try { 70 | if (DB) return DB; 71 | 72 | let dbLocation = this.RED.settings.userDir; 73 | if (!dbLocation) throw new Error('Could not find userDir to use for database store'); 74 | dbLocation += '/node-red-contrib-home-assistant.json'; 75 | 76 | const adapter = new FileAsync(dbLocation); 77 | DB = await low(adapter); 78 | DB.defaults({ nodes: {} }); 79 | return DB; 80 | } catch (e) { throw e } 81 | } 82 | 83 | // Subclasses should override these as hooks into common events 84 | onClose(removed) {} 85 | onInput() {} 86 | get nodeDbId() { 87 | return `nodes.${this.id.replace('.', '_')}`; 88 | } 89 | // namespaces data by nodeid to the lowdb store 90 | async saveNodeData(key, value) { 91 | if (!this.id || !key) throw new Error('cannot persist data to db without id and key'); 92 | const path = `${this.nodeDbId}.${key}`; 93 | const db = await this.getDb(); 94 | return db.set(path, value).write(); 95 | } 96 | async getNodeData(key) { 97 | if (!this.id) throw new Error('cannot get node data from db without id'); 98 | const db = await this.getDb(); 99 | let path = `${this.nodeDbId}`; 100 | if (key) path = path + `.${key}`; 101 | 102 | return db.get(path).value(); 103 | } 104 | async removeNodeData() { 105 | if (!this.id) throw new Error('cannot get node data from db without id'); 106 | const db = await this.getDb(); 107 | return db.remove(this.nodeDbId).write(); 108 | } 109 | 110 | send() { this.node.send(...arguments) } 111 | 112 | setStatus(opts = { shape: 'dot', fill: 'blue', text: '' }) { 113 | this.node.status(opts); 114 | } 115 | 116 | updateConnectionStatus(additionalText) { 117 | this.setConnectionStatus(this.isConnected, additionalText); 118 | } 119 | 120 | setConnectionStatus(isConnected, additionalText) { 121 | let connectionStatus = isConnected 122 | ? { shape: 'dot', fill: 'green', text: 'connected' } 123 | : { shape: 'ring', fill: 'red', text: 'disconnected' }; 124 | if (this.hasOwnProperty('isenabled') && this.isenabled === false) { 125 | connectionStatus.text += '(DISABLED)'; 126 | } 127 | 128 | if (additionalText) connectionStatus.text += ` ${additionalText}`; 129 | 130 | this.setStatus(connectionStatus); 131 | } 132 | 133 | // Hack to get around the fact that node-red only sends warn / error to the debug tab 134 | debugToClient(debugMsg) { 135 | if (!this.nodeConfig.debugenabled) return; 136 | for (let msg of arguments) { 137 | const debugMsgObj = { 138 | id: this.id, 139 | name: this.name || '', 140 | msg 141 | }; 142 | this.RED.comms.publish('debug', debugMsgObj); 143 | } 144 | } 145 | 146 | debug() { 147 | super.debug(...arguments); 148 | } 149 | 150 | get eventsClient() { 151 | return this.reach('nodeConfig.server.events'); 152 | } 153 | 154 | get isConnected() { 155 | return this.eventsClient && this.eventsClient.connected; 156 | } 157 | 158 | // Returns the evaluated path on this class instance 159 | // ex: myNode.reach('nodeConfig.server.events') 160 | reach(path) { 161 | return selectn(path, this); 162 | } 163 | } 164 | 165 | const _internals = { 166 | parseInputMessage(inputOptions, msg) { 167 | if (!inputOptions) return; 168 | let parsedResult = {}; 169 | 170 | for (let [fieldKey, fieldConfig] of Object.entries(inputOptions)) { 171 | // Try to load from message 172 | let result = { key: fieldKey, value: selectn(fieldConfig.messageProp, msg), source: 'message', validation: null }; 173 | 174 | // If message missing value and node has config that can be used instead 175 | if (result.value === undefined && fieldConfig.configProp) { 176 | result.value = selectn(fieldConfig.configProp, this.nodeConfig); 177 | result.source = 'config'; 178 | } 179 | 180 | if (!result.value && fieldConfig.default) { 181 | result.value = (typeof fieldConfig.default === 'function') 182 | ? fieldConfig.default.call(this) 183 | : fieldConfig.default; 184 | result.source = 'default'; 185 | } 186 | 187 | // If value not found in both config and message 188 | if (result.value === undefined) { 189 | result.source = 'missing'; 190 | } 191 | 192 | // If validation for value is configured run validation, optionally throwing on failed validation 193 | if (fieldConfig.validation) { 194 | const { error, value } = Joi.validate(result.value, fieldConfig.validation.schema, { convert: true }); 195 | if (error && fieldConfig.validation.haltOnFail) throw error; 196 | result.validation = { error, value }; 197 | } 198 | 199 | // Assign result to config key value 200 | parsedResult[fieldKey] = result; 201 | } 202 | 203 | return parsedResult; 204 | } 205 | }; 206 | 207 | const _eventHandlers = { 208 | preOnInput(message) { 209 | try { 210 | const parsedMessage = _internals.parseInputMessage.call(this, this.options.input, message); 211 | 212 | this.onInput({ 213 | parsedMessage, 214 | message 215 | }); 216 | } catch (e) { 217 | if (e && e.isJoi) { 218 | this.node.warn(e.message); 219 | return this.send(null); 220 | } 221 | throw e; 222 | } 223 | }, 224 | 225 | async preOnClose(removed, done) { 226 | this.debug(`closing node. Reason: ${removed ? 'node deleted' : 'node re-deployed'}`); 227 | try { 228 | await this.onClose(removed); 229 | done(); 230 | } catch (e) { 231 | this.error(e.message); 232 | } 233 | } 234 | }; 235 | 236 | module.exports = BaseNode; 237 | -------------------------------------------------------------------------------- /lib/events-node.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash.merge'); 2 | const BaseNode = require('./base-node'); 3 | 4 | const DEFAULT_NODE_OPTIONS = { 5 | debug: false, 6 | config: { 7 | name: {}, 8 | server: { isNode: true } 9 | } 10 | }; 11 | 12 | class EventsNode extends BaseNode { 13 | constructor(nodeDefinition, RED, nodeOptions = {}) { 14 | nodeOptions = merge({}, DEFAULT_NODE_OPTIONS, nodeOptions); 15 | super(nodeDefinition, RED, nodeOptions); 16 | this.listeners = {}; 17 | 18 | this.addEventClientListener({ event: 'ha_events:close', handler: this.onHaEventsClose.bind(this) }); 19 | this.addEventClientListener({ event: 'ha_events:open', handler: this.onHaEventsOpen.bind(this) }); 20 | this.addEventClientListener({ event: 'ha_events:error', handler: this.onHaEventsError.bind(this) }); 21 | 22 | this.updateConnectionStatus(); 23 | } 24 | 25 | addEventClientListener({ event, handler }) { 26 | this.listeners[event] = handler; 27 | this.eventsClient.addListener(event, handler); 28 | } 29 | 30 | onClose(nodeRemoved) { 31 | if (this.eventsClient) { 32 | Object.entries(this.listeners).forEach(([event, handler]) => { 33 | this.eventsClient.removeListener(event, handler); 34 | }); 35 | } 36 | } 37 | 38 | onHaEventsClose() { 39 | this.updateConnectionStatus(); 40 | } 41 | onHaEventsOpen() { 42 | this.updateConnectionStatus(); 43 | } 44 | 45 | onHaEventsError(err) { 46 | if (err.message) this.error(err.message); 47 | } 48 | }; 49 | 50 | module.exports = EventsNode; 51 | -------------------------------------------------------------------------------- /lib/timed-task.js: -------------------------------------------------------------------------------- 1 | const serialize = require('serialize-javascript'); 2 | 3 | class TimedTask { 4 | constructor(o) { 5 | this.id = o.id; 6 | this.task = o.task; 7 | this.onStart = o.onStart; 8 | this.onComplete = o.onComplete; 9 | this.onCancel = o.onCancel; 10 | this.onFailed = o.onFailed; 11 | 12 | const now = this.getNow(); 13 | if (o.runsAt && (o.runsAt <= now)) throw new Error('Cannot schedule a task which is set to run in the past'); 14 | 15 | let runsIn = (o.runsAt) 16 | ? o.runsAt - now 17 | : o.runsIn; 18 | 19 | this.schedule(runsIn); 20 | } 21 | 22 | schedule(runsInMs) { 23 | this.cancel(); 24 | this.scheduledAt = this.getNow(); 25 | this.runsAt = this.scheduledAt + runsInMs; 26 | this.timer = setTimeout(this.start.bind(this), runsInMs); 27 | } 28 | 29 | getNow() { 30 | return Date.now(); 31 | } 32 | 33 | cancel() { 34 | if (this.timer) { 35 | if (typeof this.onCancel === 'function') this.onCancel(); 36 | clearTimeout(this.timer); 37 | ['timer', 'scheduledAt', 'runsAt'].forEach(k => (this[k] = null)); 38 | } 39 | } 40 | 41 | getRunsAtDate() { 42 | return new Date(this.runsAt); 43 | } 44 | 45 | async start() { 46 | if (typeof this.onStart === 'function') this.onStart(); 47 | try { 48 | await this.task(); 49 | this.onComplete(); 50 | } catch (e) { 51 | if (typeof this.onFailed === 'function') { 52 | this.cancel(); 53 | this.onFailed(e); 54 | } else { throw e } 55 | } 56 | } 57 | static create(o) { 58 | return new TimedTask(o); 59 | } 60 | 61 | static createFromSerialized(str) { 62 | const taskObj = eval('(' + str + ')'); // eslint-disable-line 63 | return new TimedTask(taskObj); 64 | } 65 | 66 | serialize() { 67 | return serialize(this); 68 | } 69 | 70 | // Called by stringify with serialize 71 | toJSON() { 72 | const { id, runsAt, task, onStart, onComplete, onCancel, onFailed } = this; 73 | return { id, runsAt, task, onStart, onComplete, onCancel, onFailed }; 74 | } 75 | } 76 | 77 | module.exports = TimedTask; 78 | -------------------------------------------------------------------------------- /nodes/api_call-service/api_call-service.html: -------------------------------------------------------------------------------- 1 | 171 | 172 | 173 | 226 | 227 | 276 | -------------------------------------------------------------------------------- /nodes/api_call-service/api_call-service.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | const BaseNode = require('../../lib/base-node'); 3 | 4 | module.exports = function(RED) { 5 | const nodeOptions = { 6 | debug: true, 7 | config: { 8 | service_domain: {}, 9 | service: {}, 10 | data: {}, 11 | mergecontext: {}, 12 | name: {}, 13 | server: { isNode: true } 14 | } 15 | }; 16 | 17 | class CallServiceNode extends BaseNode { 18 | constructor(nodeDefinition) { 19 | super(nodeDefinition, RED, nodeOptions); 20 | } 21 | isObjectLike (v) { 22 | return (v !== null) && (typeof v === 'object'); 23 | } 24 | // Disable connection status for api node 25 | setConnectionStatus() {} 26 | tryToObject(v) { 27 | if (!v) return null; 28 | try { 29 | return JSON.parse(v); 30 | } catch (e) { 31 | return v; 32 | } 33 | } 34 | onInput({ message }) { 35 | let payload, payloadDomain, payloadService; 36 | 37 | if (message && message.payload) { 38 | payload = this.tryToObject(message.payload); 39 | payloadDomain = this.utils.reach('domain', payload); 40 | payloadService = this.utils.reach('service', payload); 41 | } 42 | const configDomain = this.nodeConfig.service_domain; 43 | const configService = this.nodeConfig.service; 44 | 45 | const apiDomain = payloadDomain || configDomain; 46 | const apiService = payloadService || configService; 47 | const apiData = this.getApiData(payload); 48 | if (!apiDomain) throw new Error('call service node is missing api "domain" property, not found in config or payload'); 49 | if (!apiService) throw new Error('call service node is missing api "service" property, not found in config or payload'); 50 | 51 | this.debug(`Calling Service: ${apiDomain}:${apiService} -- ${JSON.stringify(apiData || {})}`); 52 | 53 | this.send({ 54 | payload: { 55 | domain: apiDomain, 56 | service: apiService, 57 | data: apiData || null 58 | } 59 | }); 60 | 61 | return this.nodeConfig.server.api.callService(apiDomain, apiService, apiData) 62 | .catch(err => { 63 | this.warn('Error calling service, home assistant api error', err); 64 | this.error('Error calling service, home assistant api error', message); 65 | }); 66 | } 67 | 68 | getApiData(payload) { 69 | let apiData; 70 | let contextData = {}; 71 | 72 | let payloadData = this.utils.reach('data', payload); 73 | let configData = this.tryToObject(this.nodeConfig.data); 74 | payloadData = payloadData || {}; 75 | configData = configData || {}; 76 | 77 | // Cacluate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning 78 | if (this.nodeConfig.mergecontext) { 79 | const ctx = this.node.context(); 80 | let flowVal = ctx.flow.get(this.nodeConfig.mergecontext); 81 | let globalVal = ctx.global.get(this.nodeConfig.mergecontext); 82 | flowVal = flowVal || {}; 83 | globalVal = globalVal || {}; 84 | contextData = this.utils.merge({}, globalVal, flowVal); 85 | } 86 | 87 | apiData = this.utils.merge({}, configData, contextData, payloadData); 88 | return apiData; 89 | } 90 | } 91 | 92 | RED.nodes.registerType('api-call-service', CallServiceNode); 93 | }; 94 | -------------------------------------------------------------------------------- /nodes/api_call-service/icons/router-wireless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/api_call-service/icons/router-wireless.png -------------------------------------------------------------------------------- /nodes/api_current-state/api_current-state.html: -------------------------------------------------------------------------------- 1 | 68 | 69 | 103 | 104 | 148 | -------------------------------------------------------------------------------- /nodes/api_current-state/api_current-state.js: -------------------------------------------------------------------------------- 1 | const BaseNode = require('../../lib/base-node'); 2 | const Joi = require('joi'); 3 | 4 | module.exports = function(RED) { 5 | const nodeOptions = { 6 | debug: true, 7 | config: { 8 | name: {}, 9 | halt_if: {}, 10 | override_topic: {}, 11 | override_payload: {}, 12 | entity_id: {}, 13 | server: { isNode: true } 14 | }, 15 | input: { 16 | entity_id: { 17 | messageProp: 'payload.entity_id', 18 | configProp: 'entity_id', // Will be used if value not found on message, 19 | validation: { 20 | haltOnFail: true, 21 | schema: Joi.string() // Validates on message if exists, Joi will also attempt coercion 22 | } 23 | } 24 | } 25 | }; 26 | 27 | class CurrentStateNode extends BaseNode { 28 | constructor(nodeDefinition) { 29 | super(nodeDefinition, RED, nodeOptions); 30 | } 31 | 32 | /* eslint-disable camelcase */ 33 | onInput({ parsedMessage, message }) { 34 | const entity_id = parsedMessage.entity_id.value; 35 | const logAndContinueEmpty = (logMsg) => { this.node.warn(logMsg); return ({ payload: {}}) }; 36 | 37 | if (!entity_id) return logAndContinueEmpty('entity ID not set, cannot get current state, sending empty payload'); 38 | 39 | const { states } = this.nodeConfig.server.homeAssistant; 40 | if (!states) return logAndContinueEmpty('local state cache missing, sending empty payload'); 41 | 42 | const currentState = states[entity_id]; 43 | if (!currentState) return logAndContinueEmpty(`entity could not be found in cache for entity_id: ${entity_id}, sending empty payload`); 44 | 45 | const shouldHaltIfState = this.nodeConfig.halt_if && (currentState.state === this.nodeConfig.halt_if); 46 | if (shouldHaltIfState) { 47 | const debugMsg = `Get current state: halting processing due to current state of ${entity_id} matches "halt if state" option`; 48 | this.debug(debugMsg); 49 | this.debugToClient(debugMsg); 50 | return null; 51 | } 52 | 53 | // default switches to true if undefined (backward compatibility 54 | const override_topic = this.nodeConfig.override_topic || true; 55 | const override_payload = this.nodeConfig.override_payload || true; 56 | 57 | if (override_topic) message.topic = entity_id; 58 | if (override_payload) message.payload = currentState.state; 59 | 60 | message.data = currentState; 61 | this.node.send(message); 62 | } 63 | } 64 | 65 | RED.nodes.registerType('api-current-state', CurrentStateNode); 66 | }; 67 | -------------------------------------------------------------------------------- /nodes/api_current-state/icons/arrow-top-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/api_current-state/icons/arrow-top-right.png -------------------------------------------------------------------------------- /nodes/api_get-history/api_get-history.html: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 91 | 92 | 150 | -------------------------------------------------------------------------------- /nodes/api_get-history/api_get-history.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const BaseNode = require('../../lib/base-node'); 3 | 4 | module.exports = function(RED) { 5 | const nodeOptions = { 6 | debug: true, 7 | config: { 8 | name: {}, 9 | server: { isNode: true }, 10 | startdate: {}, 11 | enddate: {}, 12 | entityid: {}, 13 | entityidtype: {} 14 | }, 15 | input: { 16 | startdate: { 17 | messageProp: 'startdate', 18 | configProp: 'startdate', 19 | default: () => { 20 | const yesterday = new Date(); 21 | yesterday.setDate(yesterday.getDate() - 1); 22 | return yesterday.toISOString(); 23 | }, 24 | validation: { haltOnFail: true, schema: Joi.date().optional().allow('') } 25 | }, 26 | enddate: { 27 | messageProp: 'enddate', 28 | configProp: 'enddate', 29 | validation: { haltOnFail: true, schema: Joi.date().optional().allow('') } 30 | }, 31 | entityid: { 32 | messageProp: 'entityid', 33 | configProp: 'entityid' 34 | }, 35 | entityidtype: { 36 | messageProp: 'entityidtype', 37 | configProp: 'entityidtype' 38 | } 39 | } 40 | }; 41 | 42 | class GetHistoryNode extends BaseNode { 43 | constructor(nodeDefinition) { 44 | super(nodeDefinition, RED, nodeOptions); 45 | } 46 | 47 | onInput({ parsedMessage, message }) { 48 | let { startdate, enddate, entityid, entityidtype } = parsedMessage; 49 | startdate = startdate.value; 50 | enddate = enddate.value; 51 | entityid = entityid.value; 52 | 53 | let apiRequest = (entityidtype.value === 'includes' && entityid) 54 | ? this.nodeConfig.server.api.getHistory(startdate, null, enddate, { include: new RegExp(entityid) }) 55 | : this.nodeConfig.server.api.getHistory(startdate, entityid, enddate); 56 | 57 | return apiRequest 58 | .then(res => { 59 | message.startdate = startdate; 60 | message.enddate = enddate || null; 61 | message.entityid = entityid || null; 62 | message.payload = res; 63 | this.send(message); 64 | }) 65 | .catch(err => { 66 | this.warn('Error calling service, home assistant api error', err); 67 | this.error('Error calling service, home assistant api error', message); 68 | }); 69 | } 70 | } 71 | 72 | RED.nodes.registerType('api-get-history', GetHistoryNode); 73 | }; 74 | -------------------------------------------------------------------------------- /nodes/api_get-history/icons/timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/api_get-history/icons/timer.png -------------------------------------------------------------------------------- /nodes/api_render-template/api_render-template.html: -------------------------------------------------------------------------------- 1 | 71 | 72 | 73 | 94 | 95 | 130 | -------------------------------------------------------------------------------- /nodes/api_render-template/api_render-template.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const BaseNode = require('../../lib/base-node'); 3 | 4 | module.exports = function(RED) { 5 | const nodeOptions = { 6 | debug: true, 7 | config: { 8 | template: {}, 9 | name: {}, 10 | server: { isNode: true } 11 | }, 12 | input: { 13 | template: { 14 | messageProp: 'template', 15 | configProp: 'template', 16 | validation: { haltOnFail: true, schema: Joi.string().required() } 17 | } 18 | } 19 | }; 20 | 21 | class RenderTemplateNode extends BaseNode { 22 | constructor(nodeDefinition) { 23 | super(nodeDefinition, RED, nodeOptions); 24 | } 25 | 26 | onInput({ parsedMessage, message }) { 27 | let { template } = parsedMessage; 28 | template = template.value; 29 | 30 | return this.nodeConfig.server.api.renderTemplate(template) 31 | .then(res => { 32 | message.template = template; 33 | message.payload = res; 34 | this.send(message); 35 | }) 36 | .catch(err => { 37 | this.error(`Error calling service, home assistant api error: ${err.message}`, message); 38 | }); 39 | } 40 | } 41 | 42 | RED.nodes.registerType('api-render-template', RenderTemplateNode); 43 | }; 44 | -------------------------------------------------------------------------------- /nodes/api_render-template/icons/parser-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/api_render-template/icons/parser-json.png -------------------------------------------------------------------------------- /nodes/config-server/config-server.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 77 | -------------------------------------------------------------------------------- /nodes/config-server/config-server.js: -------------------------------------------------------------------------------- 1 | const BaseNode = require('../../lib/base-node'); 2 | 3 | module.exports = function(RED) { 4 | const HomeAssistant = require('node-home-assistant'); 5 | 6 | const httpHandlers = { 7 | getEntities: function (req, res, next) { 8 | return this.homeAssistant.getStates() 9 | .then(states => res.json(JSON.stringify(Object.keys(states)))) 10 | .catch(e => this.error(e.message)); 11 | }, 12 | getStates: function (req, res, next) { 13 | return this.homeAssistant.getStates() 14 | .then(states => res.json(JSON.stringify(states))) 15 | .catch(e => this.error(e.message)); 16 | }, 17 | getServices: function (req, res, next) { 18 | return this.homeAssistant.getServices() 19 | .then(services => res.json(JSON.stringify(services))) 20 | .catch(e => this.error(e.message)); 21 | }, 22 | getEvents: function (req, res, next) { 23 | return this.homeAssistant.getEvents() 24 | .then(events => res.json(JSON.stringify(events))) 25 | .catch(e => this.error(e.message)); 26 | } 27 | }; 28 | 29 | const nodeOptions = { 30 | debug: true, 31 | config: { 32 | name: {}, 33 | url: {}, 34 | pass: {} 35 | } 36 | }; 37 | 38 | class ConfigServerNode extends BaseNode { 39 | constructor(nodeDefinition) { 40 | super(nodeDefinition, RED, nodeOptions); 41 | 42 | this.RED.httpAdmin.get('/homeassistant/entities', httpHandlers.getEntities.bind(this)); 43 | this.RED.httpAdmin.get('/homeassistant/states', httpHandlers.getStates.bind(this)); 44 | this.RED.httpAdmin.get('/homeassistant/services', httpHandlers.getServices.bind(this)); 45 | this.RED.httpAdmin.get('/homeassistant/events', httpHandlers.getEvents.bind(this)); 46 | 47 | const HTTP_STATIC_OPTS = { root: require('path').join(__dirname, '..', '/_static'), dotfiles: 'deny' }; 48 | this.RED.httpAdmin.get('/homeassistant/static/*', function(req, res) { res.sendFile(req.params[0], HTTP_STATIC_OPTS) }); 49 | 50 | this.setOnContext('states', []); 51 | this.setOnContext('services', []); 52 | this.setOnContext('events', []); 53 | this.setOnContext('isConnected', false); 54 | 55 | if (this.nodeConfig.url && !this.homeAssistant) { 56 | this.homeAssistant = new HomeAssistant({ baseUrl: this.nodeConfig.url, apiPass: this.nodeConfig.pass }, { startListening: false }); 57 | this.api = this.homeAssistant.api; 58 | this.events = this.homeAssistant.events; 59 | 60 | this.events.addListener('ha_events:close', this.onHaEventsClose.bind(this)); 61 | this.events.addListener('ha_events:open', this.onHaEventsOpen.bind(this)); 62 | this.events.addListener('ha_events:error', this.onHaEventsError.bind(this)); 63 | this.events.addListener('ha_events:state_changed', this.onHaStateChanged.bind(this)); 64 | 65 | this.homeAssistant.startListening() 66 | .catch(() => this.startListening()); 67 | } 68 | } 69 | 70 | get nameAsCamelcase() { 71 | return this.utils.toCamelCase(this.nodeConfig.name); 72 | } 73 | 74 | // This simply tries to connected every 2 seconds, after the initial connection is successful 75 | // reconnection attempts are handled by node-home-assistant. This could use some love obviously 76 | startListening() { 77 | if (this.connectionAttempts) { 78 | clearInterval(this.connectionAttempts); 79 | this.connectionAttempts = null; 80 | } 81 | 82 | this.connectionAttempts = setInterval(() => { 83 | this.homeAssistant.startListening() 84 | .then(() => { 85 | this.debug('Connected to home assistant'); 86 | clearInterval(this.connectionAttempts); 87 | this.connectionAttempts = null; 88 | }) 89 | .catch(err => this.error(`Home assistant connection failed with error: ${err.message}`)); 90 | }, 2000); 91 | } 92 | 93 | setOnContext(key, value) { 94 | let haCtx = this.context().global.get('homeassistant'); 95 | haCtx = haCtx || {}; 96 | haCtx[this.nameAsCamelcase] = haCtx[this.nameAsCamelcase] || {}; 97 | haCtx[this.nameAsCamelcase][key] = value; 98 | this.context().global.set('homeassistant', haCtx); 99 | } 100 | 101 | getFromContext(key) { 102 | let haCtx = this.context().global.get('homeassistant'); 103 | return (haCtx[this.nameAsCamelcase]) ? haCtx[this.nameAsCamelcase][key] : null; 104 | } 105 | 106 | async onHaEventsOpen() { 107 | try { 108 | let states = await this.homeAssistant.getStates(null, true); 109 | this.setOnContext('states', states); 110 | 111 | let services = await this.homeAssistant.getServices(true); 112 | this.setOnContext('services', services); 113 | 114 | let events = await this.homeAssistant.getEvents(true); 115 | this.setOnContext('events', events); 116 | 117 | this.setOnContext('isConnected', true); 118 | 119 | this.debug('config server event listener connected'); 120 | } catch (e) { 121 | this.error(e); 122 | } 123 | } 124 | 125 | onHaStateChanged(changedEntity) { 126 | const states = this.getFromContext('states'); 127 | if (states) { 128 | states[changedEntity.entity_id] = changedEntity.event.new_state; 129 | this.setOnContext('states', states); 130 | } 131 | } 132 | 133 | onHaEventsClose() { 134 | this.setOnContext('isConnected', false); 135 | this.debug('config server event listener closed'); 136 | } 137 | 138 | onHaEventsError(err) { 139 | this.setOnContext('isConnected', false); 140 | this.debug(err); 141 | } 142 | } 143 | 144 | RED.nodes.registerType('server', ConfigServerNode); 145 | }; 146 | -------------------------------------------------------------------------------- /nodes/config-server/icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/config-server/icons/home.png -------------------------------------------------------------------------------- /nodes/poll-state/icons/timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/poll-state/icons/timer.png -------------------------------------------------------------------------------- /nodes/poll-state/poll-state.html: -------------------------------------------------------------------------------- 1 | 71 | 72 | 103 | 104 | 142 | -------------------------------------------------------------------------------- /nodes/poll-state/poll-state.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | const ta = require('time-ago'); 3 | const EventsNode = require('../../lib/events-node'); 4 | 5 | module.exports = function(RED) { 6 | const nodeOptions = { 7 | config: { 8 | entity_id: {}, 9 | updateinterval: {}, 10 | outputinitially: {}, 11 | outputonchanged: {} 12 | } 13 | }; 14 | 15 | class TimeSinceStateNode extends EventsNode { 16 | constructor(nodeDefinition) { 17 | super(nodeDefinition, RED, nodeOptions); 18 | this.init(); 19 | } 20 | 21 | init() { 22 | if (!this.nodeConfig.entity_id) throw new Error('Entity ID is required'); 23 | 24 | if (!this.timer) { 25 | const interval = (!this.nodeConfig.updateinterval || parseInt(this.nodeConfig.updateinterval) < 1) ? 1 : parseInt(this.nodeConfig.updateinterval); 26 | this.timer = setInterval(this.onTimer.bind(this), interval * 1000); 27 | } 28 | 29 | if (this.nodeConfig.outputonchanged) { 30 | this.addEventClientListener({ event: `ha_events:state_changed:${this.nodeConfig.entity_id}`, handler: this.onTimer.bind(this) }); 31 | } 32 | 33 | if (this.nodeConfig.outputinitially) { 34 | process.nextTick(() => { 35 | this.onTimer(); 36 | }); 37 | } 38 | } 39 | 40 | onClose(removed) { 41 | super.onClose(); 42 | if (this.timer) { 43 | clearInterval(this.timer); 44 | this.timer = null; 45 | } 46 | } 47 | 48 | async onTimer() { 49 | try { 50 | const state = await this.getState(this.nodeConfig.entity_id); 51 | if (!state) { 52 | this.warn(`could not find state with entity_id "${this.nodeConfig.entity_id}"`); 53 | return; 54 | } 55 | 56 | const dateChanged = this.calculateTimeSinceChanged(state); 57 | if (dateChanged) { 58 | const timeSinceChanged = ta.ago(dateChanged); 59 | const timeSinceChangedMs = Date.now() - dateChanged.getTime(); 60 | this.send({ 61 | topic: this.nodeConfig.entity_id, 62 | payload: { timeSinceChanged, timeSinceChangedMs, dateChanged, data: state } 63 | }); 64 | } else { 65 | this.warn(`could not calculate time since changed for entity_id "${this.nodeConfig.entity_id}"`); 66 | } 67 | } catch (e) { throw e } 68 | } 69 | 70 | calculateTimeSinceChanged(entityState) { 71 | const entityLastChanged = entityState.last_changed; 72 | return new Date(entityLastChanged); 73 | } 74 | // Try to fetch from cache, if not found then try and pull fresh 75 | async getState(entityId) { 76 | let state = await this.nodeConfig.server.homeAssistant.getStates(this.nodeConfig.entity_id); 77 | if (!state) { 78 | state = await this.nodeConfig.server.homeAssistant.getStates(this.nodeConfig.entity_id, true); 79 | } 80 | return state; 81 | } 82 | } 83 | RED.nodes.registerType('poll-state', TimeSinceStateNode); 84 | }; 85 | -------------------------------------------------------------------------------- /nodes/server-events-all/icons/arrow-right-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/server-events-all/icons/arrow-right-bold.png -------------------------------------------------------------------------------- /nodes/server-events-all/server-events-all.html: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 46 | 47 | 59 | -------------------------------------------------------------------------------- /nodes/server-events-all/server-events-all.js: -------------------------------------------------------------------------------- 1 | const EventsNode = require('../../lib/events-node'); 2 | 3 | module.exports = function(RED) { 4 | class ServerEventsNode extends EventsNode { 5 | constructor(nodeDefinition) { 6 | super(nodeDefinition, RED); 7 | this.addEventClientListener({ event: 'ha_events:all', handler: this.onHaEventsAll.bind(this) }); 8 | } 9 | 10 | onHaEventsAll(evt) { 11 | this.send({ event_type: evt.event_type, topic: evt.event_type, payload: evt }); 12 | } 13 | } 14 | 15 | RED.nodes.registerType('server-events', ServerEventsNode); 16 | }; 17 | -------------------------------------------------------------------------------- /nodes/server-events-state-changed/icons/arrow-right-bold-hexagon-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/server-events-state-changed/icons/arrow-right-bold-hexagon-outline.png -------------------------------------------------------------------------------- /nodes/server-events-state-changed/server-events-state-changed.html: -------------------------------------------------------------------------------- 1 | 62 | 63 | 64 | 96 | 97 | 122 | -------------------------------------------------------------------------------- /nodes/server-events-state-changed/server-events-state-changed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | const EventsNode = require('../../lib/events-node'); 3 | 4 | module.exports = function(RED) { 5 | const nodeOptions = { 6 | config: { 7 | entityidfilter: (nodeDef) => { 8 | if (!nodeDef.entityidfilter) return undefined; 9 | 10 | if (nodeDef.entityidfiltertype === 'substring') return nodeDef.entityidfilter.split(',').map(f => f.trim()); 11 | if (nodeDef.entityidfiltertype === 'regex') return new RegExp(nodeDef.entityidfilter); 12 | return nodeDef.entityidfilter; 13 | }, 14 | entityidfiltertype: {}, 15 | haltIfState: (nodeDef) => nodeDef.haltifstate ? nodeDef.haltifstate.trim() : null 16 | } 17 | }; 18 | 19 | class ServerStateChangedNode extends EventsNode { 20 | constructor(nodeDefinition) { 21 | super(nodeDefinition, RED, nodeOptions); 22 | this.addEventClientListener({ event: 'ha_events:state_changed', handler: this.onHaEventsStateChanged.bind(this) }); 23 | } 24 | 25 | onHaEventsStateChanged (evt) { 26 | try { 27 | const { entity_id, event } = evt; 28 | 29 | const shouldHaltIfState = this.shouldHaltIfState(event); 30 | const shouldIncludeEvent = this.shouldIncludeEvent(entity_id); 31 | 32 | if (shouldHaltIfState) { 33 | this.debug('flow halted due to "halt if state" setting'); 34 | return null; 35 | } 36 | 37 | const msg = { 38 | topic: entity_id, 39 | payload: event.new_state.state, 40 | data: event 41 | }; 42 | 43 | if (shouldIncludeEvent) { 44 | (event.old_state) 45 | ? this.debug(`Incoming state event: entity_id: ${event.entity_id}, new_state: ${event.new_state.state}, old_state: ${event.old_state.state}`) 46 | : this.debug(`Incoming state event: entity_id: ${event.entity_id}, new_state: ${event.new_state.state}`); 47 | 48 | return this.send(msg); 49 | } 50 | 51 | return null; 52 | } catch (e) { 53 | this.error(e); 54 | } 55 | } 56 | 57 | shouldHaltIfState (haEvent, haltIfState) { 58 | if (!this.nodeConfig.haltIfState) return false; 59 | const shouldHalt = (this.nodeConfig.haltIfState === haEvent.new_state.state); 60 | return shouldHalt; 61 | } 62 | 63 | shouldIncludeEvent (entityId) { 64 | if (!this.nodeConfig.entityidfilter) return true; 65 | const filter = this.nodeConfig.entityidfilter; 66 | const type = this.nodeConfig.entityidfiltertype; 67 | 68 | if (type === 'exact') { 69 | return filter === entityId; 70 | } 71 | 72 | if (type === 'substring') { 73 | const found = this.nodeConfig.entityidfilter.filter(filterStr => (entityId.indexOf(filterStr) >= 0)); 74 | return found.length > 0; 75 | } 76 | 77 | if (type === 'regex') { 78 | return filter.test(entityId); 79 | } 80 | } 81 | } 82 | 83 | RED.nodes.registerType('server-state-changed', ServerStateChangedNode); 84 | }; 85 | -------------------------------------------------------------------------------- /nodes/trigger-state/icons/trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AYapejian/node-red-contrib-home-assistant/c06d98b5e1eb749d6d5335eccd81f9a01f75c887/nodes/trigger-state/icons/trigger.png -------------------------------------------------------------------------------- /nodes/trigger-state/trigger-state.html: -------------------------------------------------------------------------------- 1 | 141 | 142 | 143 | 482 | 483 | 484 | 485 | 486 | 526 | -------------------------------------------------------------------------------- /nodes/trigger-state/trigger-state.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable camelcase */ 3 | const EventsNode = require('../../lib/events-node'); 4 | 5 | module.exports = function(RED) { 6 | const nodeOptions = { 7 | debug: true, 8 | config: { 9 | entityid: {}, 10 | isenabled: {}, 11 | constraints: {}, 12 | customoutputs: {} 13 | } 14 | }; 15 | 16 | class TriggerState extends EventsNode { 17 | constructor(nodeDefinition) { 18 | super(nodeDefinition, RED, nodeOptions); 19 | 20 | const eventTopic = this.eventTopic = `ha_events:state_changed:${this.nodeConfig.entityid}`; 21 | this.addEventClientListener({ event: eventTopic, handler: this.onEntityStateChanged.bind(this) }); 22 | this.NUM_DEFAULT_MESSAGES = 2; 23 | this.messageTimers = {}; 24 | 25 | this.loadPersistedData(); 26 | } 27 | 28 | onInput({ message }) { 29 | if (message === 'enable' || message.payload === 'enable') { 30 | this.isenabled = true; 31 | this.saveNodeData('isenabled', true); 32 | this.updateConnectionStatus(); 33 | return; 34 | } 35 | if (message === 'disable' || message.payload === 'disable') { 36 | this.isenabled = false; 37 | this.saveNodeData('isenabled', false); 38 | this.updateConnectionStatus(); 39 | return; 40 | } 41 | 42 | const { entity_id, new_state, old_state } = message.payload; 43 | if (entity_id && new_state && old_state) { 44 | const evt = { 45 | event_type: 'state_changed', 46 | entity_id: entity_id, 47 | event: message.payload 48 | }; 49 | 50 | this.onEntityStateChanged(evt); 51 | } 52 | } 53 | 54 | async onEntityStateChanged (eventMessage) { 55 | if (this.isenabled === false) { 56 | this.debugToClient('incoming: node is currently disabled, ignoring received event'); 57 | return; 58 | } 59 | 60 | try { 61 | const constraintComparatorResults = await this.getConstraintComparatorResults(this.nodeConfig.constraints, eventMessage); 62 | let outputs = this.getDefaultMessageOutputs(constraintComparatorResults, eventMessage); 63 | 64 | // If a constraint comparator failed we're done, also if no custom outputs to look at 65 | if (constraintComparatorResults.failed.length || !this.nodeConfig.customoutputs.length) { 66 | this.debugToClient('done processing sending messages: ', outputs); 67 | return this.send(outputs); 68 | } 69 | 70 | const customOutputsComparatorResults = this.getCustomOutputsComparatorResults(this.nodeConfig.customoutputs, eventMessage); 71 | const customOutputMessages = customOutputsComparatorResults.map(r => r.message); 72 | 73 | outputs = outputs.concat(customOutputMessages); 74 | this.debugToClient('done processing sending messages: ', outputs); 75 | this.send(outputs); 76 | } catch (e) { 77 | this.error(e); 78 | } 79 | } 80 | 81 | async getConstraintComparatorResults(constraints, eventMessage) { 82 | const comparatorResults = []; 83 | 84 | // Check constraints 85 | for (let constraint of constraints) { 86 | const { comparatorType, comparatorValue, comparatorValueDatatype, propertyValue } = constraint; 87 | const constraintTarget = await this.getConstraintTargetData(constraint, eventMessage.event); 88 | const actualValue = this.utils.reach(constraint.propertyValue, constraintTarget.state); 89 | const comparatorResult = this.getComparatorResult(comparatorType, comparatorValue, actualValue, comparatorValueDatatype); 90 | 91 | if (comparatorResult === false) { 92 | this.debugToClient(`constraint comparator: failed entity "${constraintTarget.entityid}" property "${propertyValue}" with value ${actualValue} failed "${comparatorType}" check against (${comparatorValueDatatype}) ${comparatorValue}`); // eslint-disable-line 93 | } 94 | 95 | comparatorResults.push({ constraint, constraintTarget, actualValue, comparatorResult }); 96 | } 97 | const failedComparators = comparatorResults.filter(res => !res.comparatorResult); 98 | return { all: comparatorResults || [], failed: failedComparators || [] }; 99 | } 100 | 101 | getDefaultMessageOutputs(comparatorResults, eventMessage) { 102 | const { entity_id, event } = eventMessage; 103 | 104 | const msg = { topic: entity_id, payload: event.new_state.state, data: eventMessage }; 105 | let outputs; 106 | 107 | if (comparatorResults.failed.length) { 108 | this.debugToClient('constraint comparator: one more more comparators failed to match constraints, message will send on the failed output'); 109 | 110 | msg.failedComparators = comparatorResults.failed; 111 | outputs = [null, msg]; 112 | } else { 113 | outputs = [msg, null]; 114 | } 115 | return outputs; 116 | } 117 | 118 | getCustomOutputsComparatorResults(outputs, eventMessage) { 119 | return outputs.reduce((acc, output, reduceIndex) => { 120 | let result = { output, comparatorMatched: true, actualValue: null, message: null }; 121 | 122 | if (output.comparatorPropertyType !== 'always') { 123 | result.actualValue = this.utils.reach(output.comparatorPropertyValue, eventMessage.event); 124 | result.comparatorMatched = this.getComparatorResult(output.comparatorType, output.comparatorValue, result.actualValue, output.comparatorValueDatatype); 125 | } 126 | result.message = this.getOutputMessage(result, eventMessage); 127 | acc.push(result); 128 | return acc; 129 | }, []); 130 | } 131 | 132 | async getConstraintTargetData(constraint, triggerEvent) { 133 | let targetData = { entityid: null, state: null }; 134 | try { 135 | const isTargetThisEntity = constraint.targetType === 'this_entity'; 136 | targetData.entityid = (isTargetThisEntity) ? this.nodeConfig.entityid : constraint.targetValue; 137 | 138 | // TODO: Non 'self' targets state is just new_state of an incoming event, wrap to hack around the fact 139 | // NOTE: UI needs changing to handle this there, and also to hide "previous state" if target is not self 140 | if (isTargetThisEntity) { 141 | targetData.state = triggerEvent; 142 | } else { 143 | const state = await this.nodeConfig.server.homeAssistant.getStates(targetData.entityid); 144 | targetData.state = { 145 | new_state: state 146 | }; 147 | } 148 | } catch (e) { 149 | this.debug('Error during trigger:state comparator evalutation: ', e.stack); 150 | throw e; 151 | } 152 | 153 | return targetData; 154 | } 155 | 156 | /* eslint-disable indent */ 157 | getCastValue(datatype, value) { 158 | if (!datatype) return value; 159 | 160 | switch (datatype) { 161 | case 'num': return parseFloat(value); 162 | case 'str': return value + ''; 163 | case 'bool': return !!value; 164 | case 're': return new RegExp(value); 165 | case 'list': return value.split(','); 166 | default: return value; 167 | } 168 | } 169 | 170 | /* eslint-disable indent */ 171 | getComparatorResult(comparatorType, comparatorValue, actualValue, comparatorValueDatatype) { 172 | const cValue = this.getCastValue(comparatorValueDatatype, comparatorValue); 173 | 174 | switch (comparatorType) { 175 | case 'is': 176 | case 'is_not': 177 | // Datatype might be num, bool, str, re (regular expression) 178 | const isMatch = (comparatorValueDatatype === 're') ? cValue.test(actualValue) : (cValue === actualValue); 179 | return (comparatorType === 'is') ? isMatch : !isMatch; 180 | case 'includes': 181 | case 'does_not_include': 182 | const isIncluded = cValue.includes(actualValue); 183 | return (comparatorType === 'includes') ? isIncluded : !isIncluded; 184 | case 'greater_than': 185 | return actualValue > cValue; 186 | case 'less_than': 187 | return actualValue < cValue; 188 | } 189 | } 190 | 191 | getOutputMessage({ output, comparatorMatched, actualValue }, eventMessage) { 192 | // If comparator did not match 193 | if (!comparatorMatched) { 194 | this.debugToClient(`output comparator failed: property "${output.comparatorPropertyValue}" with value ${actualValue} failed "${output.comparatorType}" check against ${output.comparatorValue}`); // eslint-disable-line 195 | return null; 196 | } 197 | 198 | if (output.messageType === 'default') { 199 | return { topic: eventMessage.entity_id, payload: eventMessage.event.new_state.state, data: eventMessage }; 200 | } 201 | 202 | try { 203 | return JSON.parse(output.messageValue); 204 | } catch (e) { 205 | return output.messageValue; 206 | } 207 | } 208 | 209 | async loadPersistedData() { 210 | try { 211 | const data = await this.getNodeData(); 212 | if (data && data.hasOwnProperty('isenabled')) { 213 | this.isenabled = data.isenabled; 214 | this.updateConnectionStatus(); 215 | } 216 | } catch (e) { 217 | this.error(e.message); 218 | } 219 | } 220 | 221 | async onClose(removed) { 222 | super.onClose(); 223 | if (removed) { 224 | await this.removeNodeData(); 225 | } 226 | } 227 | } 228 | 229 | RED.nodes.registerType('trigger-state', TriggerState); 230 | }; 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-home-assistant", 3 | "description": "node-red nodes to visually construct home automation with home assistant", 4 | "version": "0.3.2", 5 | "homepage": "https://github.com/AYapejian/node-red-contrib-home-assistant", 6 | "bugs": { 7 | "url": "https://github.com/AYapejian/node-red-contrib-home-assistant/issues" 8 | }, 9 | "scripts": { 10 | "test": "tape __tests__/**/*.spec.js | tap-min", 11 | "test:debug": "node --inspect-brk=0.0.0.0:9124 node_modules/.bin/tape __tests__/**/*.spec.js", 12 | "test:watch": "nodemon -w __tests__/ -w lib/ -w nodes/ --exec 'node node_modules/.bin/tape __tests__/**/*.spec.js'", 13 | "dev": "npm run docker:up", 14 | "dev:clean": "npm run docker:down", 15 | "docker:up": "npm run docker -- up --build --abort-on-container-exit --remove-orphans", 16 | "docker:down": "npm run docker -- down -vt5 && npm run docker -- rm -fv", 17 | "docker:restart": "npm run docker -- restart", 18 | "docker:logs": "npm run docker -- logs -f && true", 19 | "docker": "docker-compose -f docker/docker-compose.yml", 20 | "docker-map": "docker-compose -f docker/docker-compose.mapped.yml", 21 | "clean": "npm run docker:down", 22 | "lint": "eslint . ; exit 0" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/AYapejian/node-red-contrib-home-assistant" 27 | }, 28 | "license": "MIT", 29 | "keywords": [ 30 | "node-red", 31 | "home-assistant", 32 | "home assistant", 33 | "home automation" 34 | ], 35 | "node-red": { 36 | "nodes": { 37 | "server": "nodes/config-server/config-server.js", 38 | "server-events": "nodes/server-events-all/server-events-all.js", 39 | "server-state-changed": "nodes/server-events-state-changed/server-events-state-changed.js", 40 | "trigger-state": "nodes/trigger-state/trigger-state.js", 41 | "poll-state": "nodes/poll-state/poll-state.js", 42 | "api-call-service": "nodes/api_call-service/api_call-service.js", 43 | "api-current-state": "nodes/api_current-state/api_current-state.js", 44 | "api-get-history": "nodes/api_get-history/api_get-history.js", 45 | "api-render-template": "nodes/api_render-template/api_render-template.js" 46 | } 47 | }, 48 | "dependencies": { 49 | "clone-deep": "^3.0.1", 50 | "date-fns": "^1.28.5", 51 | "debug": "^2.6.3", 52 | "is-string": "^1.0.4", 53 | "joi": "^13.1.1", 54 | "lodash.merge": "^4.6.0", 55 | "lowdb": "^1.0.0", 56 | "node-home-assistant": "0.2.1", 57 | "node-red": "node-red/node-red#0.18.2", 58 | "selectn": "^1.1.2", 59 | "serialize-javascript": "^1.4.0", 60 | "time-ago": "^0.2.1" 61 | }, 62 | "devDependencies": { 63 | "eslint": "^4.8.0", 64 | "eslint-config-standard": "^11.0.0-beta.0", 65 | "eslint-plugin-import": "^2.8.0", 66 | "eslint-plugin-node": "^6.0.0", 67 | "eslint-plugin-promise": "^3.6.0", 68 | "eslint-plugin-standard": "^3.0.1", 69 | "nodemon": "^1.14.12", 70 | "should": "^13.2.1", 71 | "sinon": "^4.2.2", 72 | "supertest": "^3.0.0", 73 | "tap-min": "^1.2.2", 74 | "tape": "^4.8.0" 75 | } 76 | } 77 | --------------------------------------------------------------------------------