├── README.md ├── class.node-red-wp-data-widget.php ├── class.node-red-wp-data.php ├── class.node-red-wp-rest.php ├── class.node-red-wp-shortcodes.php ├── class.node-red-wp.php ├── index.php ├── js └── nrwp.js └── node-red-wp.php /README.md: -------------------------------------------------------------------------------- 1 | # node-red-wordpress 2 | 3 | Magical things happen when you combine *WordPress* with *Node-RED*. This plugin expands the capabilities of our [Node-RED WordPress nodes](https://github.com/Automattic/node-red-contrib-wordpress) by adding endpoints for direct communication, as well as some nifty shortcodes for displaying data in real time on the front end of your site. 4 | 5 | **Beta - Should not be used in production until endpoints are secured** 6 | 7 | ### Features 8 | - Adds REST endpoints that allow getting and setting data variables from Node-RED 9 | - Adds REST endpoints that allow fetching stats from Jetpack 10 | - Adds the `nodered_data` shortcode for adding real-time data points to the front end of your site 11 | 12 | ### API Usage 13 | 14 | The following new endpoints are exposed: 15 | 16 | - Get data by key: `/wp-json/nrwp/v1/get/` 17 | - Set data: `/wp-json/nrwp/v1/set//` 18 | - Get data keys: `/wp-json/nrwp/v1/get_keys` 19 | - Get all keys and data: `/wp-json/nrwp/v1/get_all` 20 | - Get stats (when Jetpack is enabled): `/wp-json/nrwp/v1/get_stats` 21 | 22 | Simplicity is key here. All API requests should use method `GET` and require no parameters. 23 | 24 | ### Shortcode Usage 25 | 26 | Prime your site with data using the `set` API request above. For example, make the following requests from your terminal: 27 | 28 | `curl -i https://yoursite.com/wp-json/nrwp/v1/set/temperature1/50.5` 29 | 30 | `curl -i https://yoursite.com/wp-json/nrwp/v1/set/temperature2/78.3` 31 | 32 | These data points can come from Node-RED, or directly from other IOT devices. 33 | 34 | Next, insert a shortcode in your post content: 35 | 36 | `[nodered_data key="temperature1"]` 37 | 38 | and/or 39 | 40 | `[nodered_data key="temperature2"]` 41 | 42 | Save and view your post. You'll notice the temperature reading in your post. As the value of the temperature is updated via rest, your post will reflect it in real-time, without a page refresh. 43 | 44 | ### Example Node-RED Config 45 | 46 | Import the following config into your Node-RED host. Grab a Weather Underground API key and update the config. Update the API requests to reflect your site's URL. Deploy it and watch the magic happen. 47 | 48 | ``` 49 | [{"id":"86d123de.30019","type":"tab","label":"Flow 1"},{"id":"ace1e6ea.527388","type":"bean","z":"","name":"TheBeannn","uuid":"558eebd59492402d8b1a3ee659de1b7c","connectiontype":"constant","connectiontimeout":"0"},{"id":"30a7b699.4d119a","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://api.wunderground.com/api//conditions/q/CA/Whistler_BC.json","tls":"","x":170,"y":100,"wires":[["d9ba7aee.0c67e8"]]},{"id":"dd33b62f.0a5778","type":"inject","z":"86d123de.30019","name":"","topic":"","payload":"","payloadType":"date","repeat":"300","crontab":"","once":true,"x":130,"y":40,"wires":[["30a7b699.4d119a"]]},{"id":"d9ba7aee.0c67e8","type":"function","z":"86d123de.30019","name":"foo","func":"msg.payload = JSON.parse( msg.payload );\n\nreturn msg;\n","outputs":1,"noerr":0,"x":190,"y":160,"wires":[["53bbf3b3.8fc37c"]]},{"id":"1f7f35ae.4a9a8a","type":"debug","z":"86d123de.30019","name":"","active":true,"console":"false","complete":"true","x":790,"y":440,"wires":[]},{"id":"53bbf3b3.8fc37c","type":"change","z":"86d123de.30019","name":"","rules":[{"t":"set","p":"temp_c","pt":"msg","to":"payload.current_observation.temp_c","tot":"msg"},{"t":"set","p":"description","pt":"msg","to":"payload.current_observation.weather","tot":"msg"},{"t":"set","p":"wind_string","pt":"msg","to":"payload.current_observation.wind_string","tot":"msg"},{"t":"set","p":"wind_dir","pt":"msg","to":"payload.current_observation.wind_dir","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":220,"y":220,"wires":[["c6f35a27.ac6fc8","856747b4.9fc488","fe540496.3de318","b69f3f73.8edd"]]},{"id":"c6f35a27.ac6fc8","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/whistlertemp/{{{temp_c}}}","tls":"","x":290,"y":280,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"856747b4.9fc488","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/whistlerdesc/{{{description}}}","tls":"","x":290,"y":320,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"fe540496.3de318","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/winddir/{{{wind_dir}}}","tls":"","x":290,"y":360,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"b69f3f73.8edd","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/windstring/{{{wind_string}}}","tls":"","x":290,"y":400,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"d8c4da09.1a1678","type":"bean accel","z":"86d123de.30019","name":"ReadAccel","bean":"ace1e6ea.527388","x":210,"y":540,"wires":[["be527573.bcfa38","72cc5a40.4595e4","5ed1372d.1bae68"]]},{"id":"5c2ffc17.c81f64","type":"inject","z":"86d123de.30019","name":"","topic":"","payload":"","payloadType":"date","repeat":"5","crontab":"","once":true,"x":130,"y":480,"wires":[["d8c4da09.1a1678"]]},{"id":"be527573.bcfa38","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/beanx/{{{accelX}}}","tls":"","x":390,"y":500,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"72cc5a40.4595e4","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/beany/{{{accelY}}}","tls":"","x":390,"y":540,"wires":[["1f7f35ae.4a9a8a"]]},{"id":"5ed1372d.1bae68","type":"http request","z":"86d123de.30019","name":"","method":"GET","ret":"txt","url":"http://yoursite.com/wp-json/nrwp/v1/set/beanz/{{{accelZ}}}","tls":"","x":390,"y":580,"wires":[["1f7f35ae.4a9a8a"]]}] 50 | ``` 51 | -------------------------------------------------------------------------------- /class.node-red-wp-data-widget.php: -------------------------------------------------------------------------------- 1 | 'nrwp_data_widget', 11 | 'description' => esc_html__( 'Display real time data from Node Red.', 'nrwp' ), 12 | ) 13 | ); 14 | } 15 | 16 | public function widget( $args, $instance ) { 17 | echo $args['before_widget']; 18 | 19 | if ( ! empty( $instance['title'] ) ) { 20 | echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title']; 21 | } 22 | 23 | echo sprintf( '%s', 24 | esc_attr( $instance['datakey'] ), 25 | esc_attr( $instance['datakey'] ), 26 | esc_attr( Node_Red_WP::init()->data->get( $instance['datakey'] ) ), 27 | esc_html( Node_Red_WP::init()->data->get( $instance['datakey'] ) ) 28 | ); 29 | 30 | echo $args['after_widget']; 31 | } 32 | 33 | public function form( $instance ) { 34 | $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; 35 | $datakey = ! empty( $instance['datakey'] ) ? $instance['datakey'] : ''; 36 | ?> 37 |

38 | 39 | 40 | 41 | 42 |

43 | load() 40 | * 41 | * @return string Value at key location 42 | */ 43 | function get( $key ) { 44 | $data = $this->load(); 45 | 46 | return isset( $data[ $key ] ) ? $data[ $key ] : null; 47 | } 48 | 49 | /** 50 | * Get all data keys and values 51 | * 52 | * @uses $this->load() 53 | * 54 | * @return array Array of data keys and values 55 | */ 56 | function get_all() { 57 | return $this->load(); 58 | } 59 | 60 | /** 61 | * Set a specific value to a data key, store in wp_options 62 | * 63 | * @param string $key The key location 64 | * @param string $val The value to store at key 65 | * 66 | * @uses $this->load() 67 | * @uses update_option() 68 | * @uses Node_Red_WP::DATA_OPTION_KEY 69 | * 70 | * @return bool True if option updated, false if no change 71 | */ 72 | function set( $key, $val ) { 73 | $data = $this->load(); 74 | 75 | $data[ $key ] = $val; 76 | 77 | return update_option( Node_Red_WP::DATA_OPTION_KEY, $data ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /class.node-red-wp-rest.php: -------------------------------------------------------------------------------- 1 | [a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-.]+)', array( 35 | 'methods' => 'GET', 36 | 'callback' => array( $this, 'endpoint__set' ), 37 | ) ); 38 | 39 | // Get the value for a specific key 40 | register_rest_route( 'nrwp/v1', '/get/(?P[a-zA-Z0-9-]+)', array( 41 | 'methods' => 'GET', 42 | 'callback' => array( $this, 'endpoint__get' ), 43 | ) ); 44 | 45 | // Get an array of all the data keys 46 | register_rest_route( 'nrwp/v1', '/get_keys', array( 47 | 'methods' => 'GET', 48 | 'callback' => array( $this, 'endpoint__get_keys' ), 49 | ) ); 50 | 51 | // Get the keys and values for all data points 52 | register_rest_route( 'nrwp/v1', '/get_all', array( 53 | 'methods' => 'GET', 54 | 'callback' => array( $this, 'endpoint__get_all' ), 55 | ) ); 56 | 57 | // Get stats from Jetpack 58 | register_rest_route( 'nrwp/v1', '/get_stats', array( 59 | 'methods' => 'GET', 60 | 'callback' => array( $this, 'endpoint__get_stats' ), 61 | ) ); 62 | } 63 | 64 | /** 65 | * Get the value for a specific data key 66 | * 67 | * @param WP_Rest_Request $request The REST API request object 68 | * 69 | * @uses Node_Red_WP::init() 70 | * @uses Node_Red_WP::data->get() 71 | * @uses $this->format_response() 72 | * 73 | * @return array Formatted API response 74 | */ 75 | function endpoint__get( $request ) { 76 | $nrwp = Node_Red_WP::init(); 77 | 78 | $data = $nrwp->data->get( $request['key'] ); 79 | 80 | if ( null === $data ) { 81 | return $this->format_response( false, false, true, 'No data was found for this key.' ); 82 | } 83 | 84 | return $this->format_response( true, $data ); 85 | } 86 | 87 | /** 88 | * Set the value at a specific data key 89 | * 90 | * @param WP_Rest_Request $request The REST API request object 91 | * 92 | * @uses Node_Red_WP::init() 93 | * @uses Node_Red_WP::data->set() 94 | * @uses $this->format_response() 95 | * 96 | * @return array Formatted API response 97 | */ 98 | function endpoint__set( $request ) { 99 | $nrwp = Node_Red_WP::init(); 100 | 101 | $stat = $nrwp->data->set( $request['key'], $request['val'] ); 102 | 103 | if ( $stat ) { 104 | return $this->format_response( true, false, false, 'Value updated.' ); 105 | } else { 106 | return $this->format_response( false, false, true, 'Not updated. Value did not change.' ); 107 | } 108 | } 109 | 110 | /** 111 | * Get an array of data keys 112 | * 113 | * @param WP_Rest_Request $request The REST API request object 114 | * 115 | * @uses Node_Red_WP::init() 116 | * @uses Node_Red_WP::data->get_all() 117 | * @uses $this->format_response() 118 | * 119 | * @return array Formatted API response 120 | */ 121 | function endpoint__get_keys( $request ) { 122 | $data = Node_Red_WP::init()->data->get_all(); 123 | 124 | return $this->format_response( true, array_keys( $data ) ); 125 | } 126 | 127 | /** 128 | * Get an array of data keys and their associated values 129 | * 130 | * @param WP_Rest_Request $request The REST API request object 131 | * 132 | * @uses Node_Red_WP::init() 133 | * @uses Node_Red_WP::data->get_all() 134 | * @uses $this->format_response() 135 | * 136 | * @return array Formatted API response 137 | */ 138 | function endpoint__get_all( $request ) { 139 | $nrwp = Node_Red_WP::init(); 140 | 141 | $data = $nrwp->data->get_all(); 142 | 143 | return $this->format_response( true, $data ); 144 | } 145 | 146 | /** 147 | * Get an array of stats data from Jetpack 148 | * 149 | * @param WP_Rest_Requeset $request The REST API request object 150 | * 151 | * @uses stats_get_from_restapi() 152 | * @uses $this->format_response() 153 | * 154 | * @return array Formatted API response 155 | */ 156 | function endpoint__get_stats( $request ) { 157 | if ( ! function_exists( 'stats_get_from_restapi' ) ) { 158 | return $this->format_response( false, false, true, 'This feature is not available. Please install the Jetpack plugin.' ); 159 | } 160 | 161 | $stats = stats_get_from_restapi(); 162 | 163 | return $this->format_response( true, $stats->stats ); 164 | } 165 | 166 | /** 167 | * Standardize an API response payload 168 | * 169 | * @param bool $status True if the request succeeded, false on failure 170 | * @param mixed $data False on failure, array|string on success 171 | * @param bool $error True if an error is present, false on success 172 | * @param mixed $message False if no message, string when there is an error or notice 173 | * 174 | * @return array Formatted response payload 175 | */ 176 | function format_response( $status, $data = false, $error = false, $message = false ) { 177 | return array( 178 | 'status' => $status, 179 | 'data' => $data, 180 | 'error' => (bool) $error, 181 | 'error_message' => $message, 182 | ); 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /class.node-red-wp-shortcodes.php: -------------------------------------------------------------------------------- 1 | data 32 | * @uses Node_Red_WP->data->get() 33 | * @uses esc_attr() 34 | * @uses esc_html() 35 | * 36 | * @return string Formatted shortcode markup 37 | */ 38 | function shortcode__data( $atts ) { 39 | // Get the data for the specified key 40 | $val = Node_Red_WP::init()->data->get( $atts['key'] ); 41 | 42 | // Default the data value when it's not set 43 | $val = null !== $val ? $val : '[undefined]'; 44 | 45 | // Create the shortcode markup 46 | return sprintf( '

%s

%s', 47 | esc_attr( $atts['title'] ), 48 | esc_attr( $atts['key'] ), 49 | esc_attr( $atts['key'] ), 50 | esc_html( $val ) 51 | ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /class.node-red-wp.php: -------------------------------------------------------------------------------- 1 | api = new Node_Red_WP_REST(); 48 | $this->data = new Node_Red_WP_Data(); 49 | $this->shortcodes = new Node_Red_WP_Shortcodes(); 50 | 51 | add_action( 'wp_enqueue_scripts', array( $this, 'action__enqueue_scripts' ) ); 52 | } 53 | 54 | /** 55 | * Register, localize, and include the front end Javascript 56 | * 57 | * @uses wp_register_script() 58 | * @uses wp_localize_script() 59 | * @uses wp_enqueue_script() 60 | * @uses plugin_dir_url() 61 | * @uses plugin_dir_path() 62 | * @uses esc_url() 63 | * @uses get_rest_url() 64 | */ 65 | function action__enqueue_scripts() { 66 | wp_register_script( 'nrwp', plugin_dir_url( __FILE__ ) . 'js/nrwp.js', array( 'jquery' ), filemtime( plugin_dir_path( __FILE__ ) . 'js/nrwp.js' ) ); 67 | 68 | wp_localize_script( 'nrwp', 'nrwp', array( 69 | 'resturl' => esc_url( get_rest_url() ), 70 | ) ); 71 | 72 | wp_enqueue_script( 'nrwp' ); 73 | } 74 | 75 | /** 76 | * Register our widgets 77 | * 78 | * @uses register_widget() 79 | */ 80 | function action__widgets_init() { 81 | register_widget( 'Node_Red_WP_Data_Widget' ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 1 ) { 27 | $( className ).css( 'background', '#f00' ); 28 | } else if ( parseFloat( response.data ) < -.5 || parseFloat( response.data ) > .5 ) { 29 | $( className ).css( 'background', '#faa' ); 30 | } else if ( parseFloat( response.data ) < -.1 || parseFloat( response.data ) > .1 ) { 31 | $( className ).css( 'background', '#0f0' ); 32 | } else { 33 | $( className ).css( 'background', 'transparent' ); 34 | } 35 | } 36 | } 37 | } ); 38 | } ); 39 | } 40 | 41 | // Refresh update every 3 seconds 42 | setInterval( nrwpUpdate, 3000 ); 43 | } ); -------------------------------------------------------------------------------- /node-red-wp.php: -------------------------------------------------------------------------------- 1 | Node Red and WordPress. 5 | Plugin URI: https://github.com/Automattic/node-red-wordpress 6 | Author: Automattic 7 | Author URI: https://automattic.com 8 | Version: 0.0.1 9 | */ 10 | 11 | // Class requirements 12 | require_once( plugin_dir_path( __FILE__ ) . 'class.node-red-wp.php' ); 13 | require_once( plugin_dir_path( __FILE__ ) . 'class.node-red-wp-data.php' ); 14 | require_once( plugin_dir_path( __FILE__ ) . 'class.node-red-wp-rest.php' ); 15 | require_once( plugin_dir_path( __FILE__ ) . 'class.node-red-wp-shortcodes.php' ); 16 | require_once( plugin_dir_path( __FILE__ ) . 'class.node-red-wp-data-widget.php' ); 17 | 18 | // Bootstrap the plugin flow 19 | function nrwp_init() { 20 | Node_Red_WP::init(); 21 | } 22 | add_action( 'init', 'nrwp_init' ); 23 | 24 | // Bootstrap widgets 25 | add_action( 'widgets_init', array( 'Node_Red_WP', 'action__widgets_init' ) ); --------------------------------------------------------------------------------