├── .gitignore ├── package.json ├── UNLICENSE ├── README.md └── foscam.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Franklin", 4 | "email": "fr@nkl.in", 5 | "url": "https://frankl.in" 6 | }, 7 | "name": "foscam", 8 | "description": "Remote control, view and config a Foscam/Tenvis IP camera", 9 | "version": "0.2.2", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/fvdm/nodejs-foscam.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/fvdm/nodejs-foscam/issues" 16 | }, 17 | "main": "foscam.js", 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "dotest": "^2" 21 | }, 22 | "engines": { 23 | "node": ">=18" 24 | }, 25 | "keywords": [ 26 | "camera", 27 | "foscam", 28 | "ipcam", 29 | "tenvis", 30 | "webcam" 31 | ], 32 | "license": "Unlicense" 33 | } 34 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # foscam 2 | 3 | Remote control, view and config a Foscam/Tenvis IP camera. 4 | 5 | All included methods are based on Foscam's (fragmented) API documentation. 6 | Some features may not be supported by non-pan/tilt, older cameras or old firmware. 7 | So make sure you keep a backup of your camera settings, just in case. 8 | 9 | 10 | ## Requirements 11 | 12 | - Node.js 18 or newer (uses native `fetch()`) 13 | 14 | 15 | ## Usage 16 | 17 | All methods support both **Promises** and **callbacks**. 18 | When no callback is provided, a Promise is returned. 19 | 20 | ### Promise style (async/await) 21 | 22 | ```js 23 | const cam = require( 'foscam' ); 24 | 25 | cam.setup( { 26 | host: 'mycamera.lan', 27 | port: 81, 28 | user: 'admin', 29 | pass: '', 30 | } ); 31 | 32 | // Using async/await 33 | const status = await cam.status(); 34 | console.log( status ); 35 | 36 | // Take a snapshot 37 | await cam.control.decoder( 'left' ); 38 | await cam.control.decoder( 'stop left' ); 39 | const filepath = await cam.snapshot( '/path/to/save.jpg' ); 40 | console.log( filepath ); 41 | ``` 42 | 43 | ### Callback style 44 | 45 | ```js 46 | const cam = require( 'foscam' ); 47 | 48 | cam.setup( { 49 | host: 'mycamera.lan', 50 | port: 81, 51 | user: 'admin', 52 | pass: '', 53 | } ); 54 | 55 | // start rotating left 56 | cam.control.decoder( 'left', () => { 57 | 58 | // stop rotation 59 | cam.control.decoder( 'stop left', () => { 60 | 61 | // take a picture and store it on your computer 62 | cam.snapshot( '/path/to/save.jpg', console.log ); 63 | 64 | } ); 65 | } ); 66 | ``` 67 | 68 | 69 | ## Installation 70 | 71 | Stable: `npm install foscam` 72 | 73 | Develop: `npm install fvdm/nodejs-foscam#develop` 74 | 75 | 76 | ## Methods 77 | 78 | Every method takes an optional `callback` function as last parameter. 79 | When no callback is provided, the method returns a **Promise**. 80 | 81 | **NOTE:** Some methods require a certain access-level, i.e. *admins* can do everything, but a *visitor* can only view. 82 | 83 | 84 | ### Basic 85 | 86 | ### setup 87 | #### ( properties, [callback] ) 88 | 89 | In order to connect to the camera you first need to provide its access details. You can either do this by setting the properties below directly in `cam.settings`, but better is to use `cam.setup()`. When the `callback` function is provided, `setup()` will attempt to connect to the camera and retrieve its status, returned as object to the callback. When it fails the callback gets **false**. 90 | 91 | 92 | name | type | default | description 93 | --------|--------|---------------|---------------------- 94 | host | string | 192.168.1.239 | Camera IP or hostname 95 | port | number | 81 | Camera port number 96 | user | string | admin | Username 97 | pass | string | | Password 98 | timeout | number | 5000 | Request timeout in ms 99 | 100 | 101 | ```js 102 | // Promise style 103 | const status = await cam.setup( { 104 | host: 'mycamera.lan', 105 | port: 81, 106 | user: 'admin', 107 | pass: '', 108 | } ); 109 | 110 | if ( !status ) { 111 | console.error( 'ERROR: can\'t connect' ); 112 | } 113 | else { 114 | console.log( status ); 115 | } 116 | ``` 117 | 118 | ```js 119 | // Callback style 120 | cam.setup( 121 | { 122 | host: 'mycamera.lan', 123 | port: 81, 124 | user: 'admin', 125 | pass: '', 126 | }, 127 | ( status ) => { 128 | if ( !status ) { 129 | console.error( 'ERROR: can\'t connect' ); 130 | } 131 | else { 132 | console.log( status ); 133 | } 134 | } 135 | ); 136 | ``` 137 | 138 | ### status 139 | #### ( [callback] ) 140 | 141 | **Permission: everyone** 142 | 143 | Get basic details from the camera. 144 | 145 | ```js 146 | // Promise 147 | const result = await cam.status(); 148 | console.log( result ); 149 | 150 | // Callback 151 | cam.status( console.log ); 152 | ``` 153 | 154 | ```js 155 | { id: '001A11A00A0B', 156 | sys_ver: '0.37.2.36', 157 | app_ver: '3.2.2.18', 158 | alias: 'Cam1', 159 | now: '1343304558', 160 | tz: '-3600', 161 | alarm_status: '0', 162 | ddns_status: '0', 163 | ddns_host: '', 164 | oray_type: '0', 165 | upnp_status: '0', 166 | p2p_status: '0', 167 | p2p_local_port: '23505', 168 | msn_status: '0', 169 | alarm_status_str: 'no alarm', 170 | ddns_status_str: 'No Action', 171 | upnp_status_str: 'No Action' } 172 | ``` 173 | 174 | ### camera_params 175 | #### ( [callback] ) 176 | 177 | **Permission: visitor** 178 | 179 | Get camera sensor settings. 180 | 181 | ```js 182 | // Promise 183 | const params = await cam.camera_params(); 184 | console.log( params ); 185 | 186 | // Callback 187 | cam.camera_params( console.log ); 188 | ``` 189 | 190 | ```js 191 | { resolution: 32, 192 | brightness: 96, 193 | contrast: 4, 194 | mode: 1, 195 | flip: 0, 196 | fps: 0 } 197 | ``` 198 | 199 | ### Camera 200 | 201 | ### snapshot 202 | #### ( [filename], [callback] ) 203 | 204 | Take a snapshot. Either receive the **binary JPEG** in the `callback` or specify a `filename` to store it on your computer. 205 | 206 | When a `filename` is provided the callback will return either the *filename* on success or *false* on failure. 207 | 208 | ```js 209 | // Promise - custom processing 210 | const jpeg = await cam.snapshot(); 211 | // add binary processing here 212 | 213 | // Promise - store locally 214 | const filepath = await cam.snapshot( './my_view.jpg' ); 215 | console.log( filepath ); 216 | 217 | // Callback - custom processing 218 | cam.snapshot( ( jpeg ) => { 219 | // add binary processing here 220 | } ); 221 | 222 | // Callback - store locally 223 | cam.snapshot( './my_view.jpg', console.log ); 224 | ``` 225 | 226 | 227 | ### preset.set 228 | #### ( id, [cb] ) 229 | 230 | Save current camera position in preset #`id`. You can set presets 1 to 16. 231 | 232 | ```js 233 | // Promise 234 | await cam.preset.set( 3 ); 235 | 236 | // Callback 237 | cam.preset.set( 3, console.log ); 238 | ``` 239 | 240 | 241 | ### preset.go 242 | #### ( id, [cb] ) 243 | 244 | Move camera to the position as stored in preset #`id`. You can use presets 1 to 16. 245 | 246 | ```js 247 | // Promise 248 | await cam.preset.go( 3 ); 249 | 250 | // Callback 251 | cam.preset.go( 3, console.log ); 252 | ``` 253 | 254 | 255 | ### control.decoder 256 | #### ( command, [callback] ) 257 | 258 | Control camera movement, like pan and tilt. 259 | 260 | The `command` to execute can be a string or number. 261 | 262 | 263 | command | description 264 | -----------------------|------------------ 265 | up | start moving up 266 | stop up | stop moving up 267 | down | start moving down 268 | stop down | stop moving down 269 | left | start moving left 270 | stop left | stop moving left 271 | right | start moving right 272 | stop right | stop moving right 273 | center | move to center 274 | vertical patrol | start moving y-axis 275 | stop vertical patrol | stop moving y-axis 276 | horizontal patrol | start moving x-axis 277 | stop horizontal patrol | stop moving x-axis 278 | io output high | iR on _(some cameras)_ 279 | io output low | iR off _(some camera)_ 280 | 281 | 282 | ```js 283 | // Promise 284 | await cam.control.decoder( 'horizontal patrol' ); 285 | console.log( 'Camera moving left-right' ); 286 | 287 | // Callback 288 | cam.control.decoder( 'horizontal patrol', () => { 289 | console.log( 'Camera moving left-right' ); 290 | } ); 291 | ``` 292 | 293 | 294 | ### control.camera 295 | #### ( name, value, [callback] ) 296 | 297 | Change a camera (sensor) setting. 298 | 299 | 300 | name | value 301 | -----------|----------------------------------- 302 | resolution | `240` (320x240) or `480` (640x480) 303 | brightness | `0` to `255` 304 | contrast | `0` to `6` 305 | mode | `50` Hz, `60` Hz or `outdoor` 306 | flipmirror | `default`, `flip`, `mirror` or `flipmirror` 307 | 308 | 309 | ```js 310 | // Promise 311 | await cam.control.camera( 'resolution', 640 ); 312 | console.log( 'Resolution changed to 640x480' ); 313 | 314 | // Callback 315 | cam.control.camera( 'resolution', 640, () => { 316 | console.log( 'Resolution changed to 640x480' ); 317 | } ); 318 | ``` 319 | 320 | 321 | ### System 322 | 323 | ### reboot 324 | #### ( [callback] ) 325 | 326 | Reboot the device 327 | 328 | ```js 329 | // Promise 330 | await cam.reboot(); 331 | console.log( 'Rebooting camera' ); 332 | 333 | // Callback 334 | cam.reboot( () => { 335 | console.log( 'Rebooting camera' ); 336 | } ); 337 | ``` 338 | 339 | 340 | ### restore_factory 341 | #### ( [callback] ) 342 | 343 | Reset all settings back to their factory values. 344 | 345 | ```js 346 | // Promise 347 | await cam.restore_factory(); 348 | console.log( 'Resetting camera settings to factory defaults' ); 349 | 350 | // Callback 351 | cam.restore_factory( () => { 352 | console.log( 'Resetting camera settings to factory defaults' ); 353 | } ); 354 | ``` 355 | 356 | 357 | ### talk 358 | #### ( propsObject ) 359 | 360 | Directly communicate with the device. 361 | 362 | 363 | property | type | required | value 364 | ---------|----------|----------|---------------------- 365 | path | string | yes | i.e. `get_params.cgi` 366 | fields | object | no | i.e. `{ ntp_enable: 1, ntp_svr: 'ntp.xs4all.nl' }` 367 | encoding | string | no | `binary` or `utf8` (default) 368 | callback | function | no | i.e. `( response ) => {}` 369 | timeout | number | no | Request timeout in ms (default: 5000) 370 | 371 | 372 | ```js 373 | // Promise 374 | const response = await cam.talk( { 375 | path: 'set_datetime.cgi', 376 | fields: { 377 | ntp_enable: 1, 378 | ntp_svr: 'ntp.xs4all.nl', 379 | tz: -3600, 380 | }, 381 | } ); 382 | console.log( response ); 383 | 384 | // Callback 385 | cam.talk( { 386 | path: 'set_datetime.cgi', 387 | fields: { 388 | ntp_enable: 1, 389 | ntp_svr: 'ntp.xs4all.nl', 390 | tz: -3600, 391 | }, 392 | callback: ( response ) => { 393 | console.log( response ); 394 | }, 395 | } ); 396 | ``` 397 | 398 | 399 | ## Unlicense 400 | 401 | This is free and unencumbered software released into the public domain. 402 | 403 | Anyone is free to copy, modify, publish, use, compile, sell, or 404 | distribute this software, either in source code form or as a compiled 405 | binary, for any purpose, commercial or non-commercial, and by any 406 | means. 407 | 408 | In jurisdictions that recognize copyright laws, the author or authors 409 | of this software dedicate any and all copyright interest in the 410 | software to the public domain. We make this dedication for the benefit 411 | of the public at large and to the detriment of our heirs and 412 | successors. We intend this dedication to be an overt act of 413 | relinquishment in perpetuity of all present and future rights to this 414 | software under copyright law. 415 | 416 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 417 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 418 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 419 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 420 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 421 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 422 | OTHER DEALINGS IN THE SOFTWARE. 423 | 424 | For more information, please refer to 425 | 426 | 427 | ## Author 428 | 429 | Franklin van de Meent 430 | | [Website](https://frankl.in) 431 | | [Github](https://github.com/fvdm) 432 | -------------------------------------------------------------------------------- /foscam.js: -------------------------------------------------------------------------------- 1 | /* 2 | Name: foscam 3 | Description: Remote control a Foscam/Tenvis IP camera 4 | Framework: node.js 5 | Author: Franklin van de Meent (https://frankl.in) 6 | Source & docs: https://github.com/fvdm/nodejs-foscam 7 | Feedback: https://github.com/fvdm/nodejs-foscam/issues 8 | License: Unlicense (public domain) - see LICENSE file 9 | */ 10 | 11 | const fs = require( 'fs' ); 12 | const { EventEmitter } = require( 'events' ); 13 | const app = new EventEmitter(); 14 | 15 | // defaults 16 | app.settings = { 17 | host: '192.168.1.239', 18 | port: 81, 19 | user: 'admin', 20 | pass: '', 21 | timeout: 5000, 22 | }; 23 | 24 | // overrides 25 | app.setup = function( props, cb ) { 26 | for ( const key in props ) { 27 | app.settings[key] = props[key]; 28 | } 29 | 30 | if ( typeof cb === 'function' ) { 31 | return app.status( cb ); 32 | } 33 | }; 34 | 35 | 36 | // status 37 | app.status = function( cb ) { 38 | const processData = ( data ) => { 39 | const result = {}; 40 | const lines = data.split( '\n' ); 41 | 42 | for ( const line of lines ) { 43 | if ( line !== '' ) { 44 | const parts = line.split( 'var ' ); 45 | const parsed = String( parts[1] ).split( '=' ); 46 | parsed[1] = String( parsed[1] ).replace( /;$/, '' ); 47 | result[parsed[0]] = parsed[1].substring( 0, 1 ) === '\'' ? parsed[1].substring( 1, parsed[1].length - 2 ) : parsed[1]; 48 | } 49 | } 50 | 51 | if ( result.alarm_status ) { 52 | switch ( result.alarm_status ) { 53 | case '0': result.alarm_status_str = 'no alarm'; break; 54 | case '1': result.alarm_status_str = 'motion alarm'; break; 55 | case '2': result.alarm_status_str = 'input alarm'; break; 56 | } 57 | } 58 | 59 | if ( result.ddns_status ) { 60 | switch ( result.ddns_status ) { 61 | case '0': result.ddns_status_str = 'No Action'; break; 62 | case '1': result.ddns_status_str = 'It\'s connecting...'; break; 63 | case '2': result.ddns_status_str = 'Can\'t connect to the Server'; break; 64 | case '3': result.ddns_status_str = 'Dyndns Succeed'; break; 65 | case '4': result.ddns_status_str = 'DynDns Failed: Dyndns.org Server Error'; break; 66 | case '5': result.ddns_status_str = 'DynDns Failed: Incorrect User or Password'; break; 67 | case '6': result.ddns_status_str = 'DynDns Failed: Need Credited User'; break; 68 | case '7': result.ddns_status_str = 'DynDns Failed: Illegal Host Format'; break; 69 | case '8': result.ddns_status_str = 'DynDns Failed: The Host Does not Exist'; break; 70 | case '9': result.ddns_status_str = 'DynDns Failed: The Host Does not Belong to You'; break; 71 | case '10': result.ddns_status_str = 'DynDns Failed: Too Many or Too Few Hosts'; break; 72 | case '11': result.ddns_status_str = 'DynDns Failed: The Host is Blocked for Abusing'; break; 73 | case '12': result.ddns_status_str = 'DynDns Failed: Bad Reply from Server'; break; 74 | case '13': result.ddns_status_str = 'DynDns Failed: Bad Reply from Server'; break; 75 | case '14': result.ddns_status_str = 'Oray Failed: Bad Reply from Server'; break; 76 | case '15': result.ddns_status_str = 'Oray Failed: Incorrect User or Password'; break; 77 | case '16': result.ddns_status_str = 'Oray Failed: Incorrect Hostname'; break; 78 | case '17': result.ddns_status_str = 'Oray Succeed'; break; 79 | case '18': result.ddns_status_str = 'Reserved'; break; 80 | case '19': result.ddns_status_str = 'Reserved'; break; 81 | case '20': result.ddns_status_str = 'Reserved'; break; 82 | case '21': result.ddns_status_str = 'Reserved'; break; 83 | } 84 | } 85 | 86 | if ( result.upnp_status ) { 87 | switch ( result.upnp_status ) { 88 | case '0': result.upnp_status_str = 'No Action'; break; 89 | case '1': result.upnp_status_str = 'Succeed'; break; 90 | case '2': result.upnp_status_str = 'Device System Error'; break; 91 | case '3': result.upnp_status_str = 'Errors in Network Communication'; break; 92 | case '4': result.upnp_status_str = 'Errors in Chat with UPnP Device'; break; 93 | case '5': result.upnp_status_str = 'Rejected by UPnP Device, Maybe Port Conflict'; break; 94 | } 95 | } 96 | 97 | return result; 98 | }; 99 | 100 | if ( typeof cb === 'function' ) { 101 | app.talk( { 102 | path: 'get_status.cgi', 103 | callback: ( data ) => cb( processData( data ) ) 104 | } ); 105 | return; 106 | } 107 | 108 | return app.talk( { path: 'get_status.cgi' } ).then( processData ); 109 | }; 110 | 111 | 112 | // camera params 113 | app.camera_params = function( cb ) { 114 | const processData = ( data ) => { 115 | const result = {}; 116 | data.replace( /var ([^=]+)=([^;]+);/g, ( str, key, value ) => { 117 | const parsed = parseInt( value, 10 ); 118 | result[key] = isNaN( parsed ) ? 0 : parsed; 119 | } ); 120 | return result; 121 | }; 122 | 123 | if ( typeof cb === 'function' ) { 124 | app.talk( { 125 | path: 'get_camera_params.cgi', 126 | callback: ( data ) => cb( processData( data ) ) 127 | } ); 128 | return; 129 | } 130 | 131 | return app.talk( { path: 'get_camera_params.cgi' } ).then( processData ); 132 | }; 133 | 134 | 135 | // Presets 136 | app.preset = { 137 | id2cmd: function( action, id ) { 138 | const cmds = { 139 | set: [30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60], 140 | go: [31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61] 141 | }; 142 | return cmds[action][id - 1]; 143 | }, 144 | 145 | set: function( id, cb ) { 146 | return app.control.decoder( app.preset.id2cmd( 'set', id ), cb ); 147 | }, 148 | 149 | go: function( id, cb ) { 150 | return app.control.decoder( app.preset.id2cmd( 'go', id ), cb ); 151 | } 152 | }; 153 | 154 | 155 | // control 156 | app.control = { 157 | 158 | // pan/tilt 159 | decoder: function( cmd, cb ) { 160 | let command = cmd; 161 | 162 | if ( typeof command === 'string' && !command.match( /^[0-9]+$/ ) ) { 163 | switch ( command ) { 164 | case 'up': command = 0; break; 165 | case 'stop up': command = 1; break; 166 | case 'down': command = 2; break; 167 | case 'stop down': command = 3; break; 168 | case 'left': command = 4; break; 169 | case 'stop left': command = 5; break; 170 | case 'right': command = 6; break; 171 | case 'stop right': command = 7; break; 172 | case 'center': command = 25; break; 173 | case 'vertical patrol': command = 26; break; 174 | case 'stop vertical patrol': command = 27; break; 175 | case 'horizontal patrol': command = 28; break; 176 | case 'stop horizontal patrol': command = 29; break; 177 | case 'io output high': command = 94; break; 178 | case 'io output low': command = 95; break; 179 | } 180 | } 181 | 182 | if ( typeof cb === 'function' ) { 183 | app.talk( { 184 | path: 'decoder_control.cgi', 185 | fields: { command }, 186 | callback: cb 187 | } ); 188 | return; 189 | } 190 | 191 | return app.talk( { 192 | path: 'decoder_control.cgi', 193 | fields: { command } 194 | } ); 195 | }, 196 | 197 | // camera settings 198 | camera: function( param, value, cb ) { 199 | let paramVal = param; 200 | let valueVal = value; 201 | 202 | // fix param 203 | if ( typeof paramVal === 'string' && !paramVal.match( /^[0-9]+$/ ) ) { 204 | switch ( paramVal ) { 205 | 206 | case 'brightness': paramVal = 1; break; 207 | case 'contrast': paramVal = 2; break; 208 | 209 | // resolution 210 | case 'resolution': 211 | paramVal = 0; 212 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]{1,2}$/ ) ) { 213 | switch ( valueVal ) { 214 | case '320': 215 | case '320x240': 216 | case '320*240': 217 | valueVal = 8; 218 | break; 219 | 220 | case '640': 221 | case '640x480': 222 | case '640*480': 223 | valueVal = 32; 224 | break; 225 | } 226 | } 227 | break; 228 | 229 | case 'mode': 230 | paramVal = 3; 231 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]$/ ) ) { 232 | switch ( valueVal.toLowerCase() ) { 233 | case '50': 234 | case '50hz': 235 | case '50 hz': 236 | valueVal = 0; 237 | break; 238 | 239 | case '60': 240 | case '60hz': 241 | case '60 hz': 242 | valueVal = 1; 243 | break; 244 | 245 | case 'outdoor': 246 | case 'outside': 247 | valueVal = 2; 248 | break; 249 | } 250 | } 251 | break; 252 | 253 | case 'flipmirror': 254 | paramVal = 5; 255 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]$/ ) ) { 256 | switch ( valueVal.toLowerCase() ) { 257 | case 'default': 258 | valueVal = 0; 259 | break; 260 | 261 | case 'flip': 262 | valueVal = 1; 263 | break; 264 | 265 | case 'mirror': 266 | valueVal = 2; 267 | break; 268 | 269 | case 'flipmirror': 270 | case 'flip&mirror': 271 | case 'flip+mirror': 272 | case 'flip + mirror': 273 | case 'flip & mirror': 274 | valueVal = 3; 275 | break; 276 | } 277 | } 278 | break; 279 | } 280 | } 281 | 282 | // send it 283 | if ( typeof cb === 'function' ) { 284 | app.talk( { 285 | path: 'camera_control.cgi', 286 | fields: { 287 | param: paramVal, 288 | value: valueVal 289 | }, 290 | callback: cb 291 | } ); 292 | return; 293 | } 294 | 295 | return app.talk( { 296 | path: 'camera_control.cgi', 297 | fields: { 298 | param: paramVal, 299 | value: valueVal 300 | } 301 | } ); 302 | } 303 | }; 304 | 305 | 306 | // reboot 307 | app.reboot = function( cb ) { 308 | if ( typeof cb === 'function' ) { 309 | app.talk( { 310 | path: 'reboot.cgi', 311 | callback: cb 312 | } ); 313 | return; 314 | } 315 | 316 | return app.talk( { path: 'reboot.cgi' } ); 317 | }; 318 | 319 | 320 | // restore factory 321 | app.restore_factory = function( cb ) { 322 | if ( typeof cb === 'function' ) { 323 | app.talk( { 324 | path: 'restore_factory.cgi', 325 | callback: cb 326 | } ); 327 | return; 328 | } 329 | 330 | return app.talk( { path: 'restore_factory.cgi' } ); 331 | }; 332 | 333 | 334 | // params 335 | app.params = function( cb ) { 336 | if ( typeof cb === 'function' ) { 337 | app.talk( { 338 | path: 'get_params.cgi', 339 | callback: cb 340 | } ); 341 | return; 342 | } 343 | 344 | return app.talk( { path: 'get_params.cgi' } ); 345 | }; 346 | 347 | 348 | // set 349 | app.set = { 350 | 351 | // alias 352 | alias: function( alias, cb ) { 353 | if ( typeof cb === 'function' ) { 354 | app.talk( { 355 | path: 'set_alias.cgi', 356 | fields: { alias }, 357 | callback: cb 358 | } ); 359 | return; 360 | } 361 | 362 | return app.talk( { 363 | path: 'set_alias.cgi', 364 | fields: { alias } 365 | } ); 366 | }, 367 | 368 | // datetime 369 | datetime: function( props, cb ) { 370 | if ( typeof cb === 'function' ) { 371 | app.talk( { 372 | path: 'set_datetime.cgi', 373 | fields: props, 374 | callback: cb 375 | } ); 376 | return; 377 | } 378 | 379 | return app.talk( { 380 | path: 'set_datetime.cgi', 381 | fields: props 382 | } ); 383 | } 384 | }; 385 | 386 | 387 | // snapshot 388 | app.snapshot = function( filepath, cb ) { 389 | let callback = cb; 390 | let savePath = filepath; 391 | 392 | if ( !callback && typeof filepath === 'function' ) { 393 | callback = filepath; 394 | savePath = false; 395 | } 396 | 397 | const processData = async ( bin ) => { 398 | if ( savePath ) { 399 | await fs.promises.writeFile( savePath, bin ); 400 | return savePath; 401 | } 402 | return bin; 403 | }; 404 | 405 | if ( typeof callback === 'function' ) { 406 | app.talk( { 407 | path: 'snapshot.cgi', 408 | encoding: 'binary', 409 | callback: async ( bin ) => { 410 | try { 411 | const result = await processData( bin ); 412 | callback( result ); 413 | } 414 | catch ( err ) { 415 | app.emit( 'connection-error', err ); 416 | callback( false ); 417 | } 418 | } 419 | } ); 420 | return; 421 | } 422 | 423 | return app.talk( { 424 | path: 'snapshot.cgi', 425 | encoding: 'binary' 426 | } ).then( processData ); 427 | }; 428 | 429 | 430 | // communicate 431 | app.talk = function( { 432 | 433 | path, 434 | fields = {}, 435 | encoding = '', 436 | callback = false, 437 | timeout = app.settings.timeout, 438 | 439 | } ) { 440 | 441 | fields.user = app.settings.user; 442 | fields.pwd = app.settings.pass; 443 | 444 | const queryParams = new URLSearchParams( fields ).toString(); 445 | const url = `http://${app.settings.host}:${app.settings.port}/${path}?${queryParams}`; 446 | 447 | const options = { 448 | timeout: AbortSignal.timeout( parseInt( timeout, 10 ) ), 449 | }; 450 | 451 | const fetchData = async () => { 452 | try { 453 | const response = await fetch( url, options ); 454 | 455 | if ( !response.ok ) { 456 | throw new Error( `HTTP error! status: ${response.status}` ); 457 | } 458 | 459 | let data; 460 | if ( encoding === 'binary' ) { 461 | const buffer = await response.arrayBuffer(); 462 | data = Buffer.from( buffer ); 463 | } 464 | else { 465 | data = await response.text(); 466 | data = data.trim(); 467 | } 468 | 469 | return data; 470 | } 471 | catch ( err ) { 472 | app.emit( 'connection-error', err ); 473 | throw err; 474 | } 475 | }; 476 | 477 | if ( typeof callback === 'function' ) { 478 | fetchData().then( callback ).catch( () => {} ); 479 | return; 480 | } 481 | 482 | return fetchData(); 483 | 484 | }; 485 | 486 | // ready 487 | module.exports = app; 488 | --------------------------------------------------------------------------------