├── files ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png └── 7.png ├── module.json ├── examples ├── readme.md ├── add_input.json └── reset2original.json ├── device_menu.php ├── .travis.yml ├── composer.json ├── device_schema.php ├── data ├── OpenEnergyMonitor │ ├── emontx-HP.json │ ├── emonpi-HEM.json │ ├── emontx-HEM.json │ ├── emonth.json │ ├── emonpi-SPV2.json │ ├── emonpi-SPV1.json │ ├── emontx-SPV2.json │ └── emontx-SPV1.json ├── Control │ ├── wifirelay.json │ └── smartplug.json ├── CircuitSetup │ ├── circuitsetup_split-phase.json │ ├── circuitsetup-6_channel.json │ ├── circuitsetup-split-phase_solar.json │ └── circuitsetup-6_channel_solar.json ├── OpenEVSE │ └── openevse.json ├── amrm.json └── amig.json ├── Views ├── device.js ├── device_api.php ├── device_view.php ├── device_dialog.css ├── device_dialog.php └── device_dialog.js ├── README.md ├── device_controller.php ├── device_template.php └── device_model.php /files/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/1.png -------------------------------------------------------------------------------- /files/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/2.png -------------------------------------------------------------------------------- /files/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/3.png -------------------------------------------------------------------------------- /files/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/4.png -------------------------------------------------------------------------------- /files/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/5.png -------------------------------------------------------------------------------- /files/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/6.png -------------------------------------------------------------------------------- /files/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/device/master/files/7.png -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Device", 3 | "version" : "2.0.7" 4 | } 5 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | These are some very simple device files that show how specific functions work. 2 | 3 | It's not an exhaustive list, but hopefully it's indicative of the style. 4 | -------------------------------------------------------------------------------- /device_menu.php: -------------------------------------------------------------------------------- 1 | _("Device Setup"), 5 | 'path' => 'device/view', 6 | 'icon' => 'device', 7 | 'order' => 'b6' 8 | );*/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "7.0" 4 | - "7.1" 5 | - "7.2" 6 | - "7.3" 7 | env: 8 | global: 9 | - COMPOSER_DISABLE_XDEBUG_WARN=1 10 | install: 11 | - composer install 12 | script: 13 | - composer test 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoncms/device", 3 | "homepage": "https://emoncms.org", 4 | "support": { 5 | "forum": "https://community.openenergymonitor.org/" 6 | }, 7 | "require-dev": { 8 | "php-parallel-lint/php-parallel-lint": "^1.2.0" 9 | }, 10 | "scripts": { 11 | "test": [ 12 | "parallel-lint . --exclude vendor" 13 | ] 14 | }, 15 | "license": "AGPL-3.0-or-later" 16 | } 17 | -------------------------------------------------------------------------------- /device_schema.php: -------------------------------------------------------------------------------- 1 | array('type' => 'int(11)', 'Null'=>false, 'Key'=>'PRI', 'Extra'=>'auto_increment'), 5 | 'userid' => array('type' => 'int(11)'), 6 | 'nodeid' => array('type' => 'text'), 7 | 'name' => array('type' => 'text'), 8 | 'description' => array('type' => 'text'), 9 | 'type' => array('type' => 'varchar(32)'), 10 | 'devicekey' => array('type' => 'varchar(64)'), 11 | 'time' => array('type' => 'int(10)') 12 | ); 13 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emontx-HP.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Heatpump Monitor", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonTx", 5 | "description": "Heatpump Monitor firmware for EmonTx v3", 6 | "inputs": [ 7 | { 8 | "name": "P1", 9 | "description": "Consumption", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 14 | }, 15 | { 16 | "process": "power_to_kwh", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 18 | } 19 | ] 20 | } 21 | ], 22 | 23 | "feeds": [ 24 | { 25 | "name": "use", 26 | "type": "DataType::REALTIME", 27 | "engine": "Engine::PHPFINA", 28 | "interval": "10", 29 | "unit": "W" 30 | }, 31 | { 32 | "name": "use_kwh", 33 | "type": "DataType::REALTIME", 34 | "engine": "Engine::PHPFINA", 35 | "interval": "10", 36 | "unit": "kWh" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emonpi-HEM.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HomeEnergyMonitor", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonPi", 5 | "description": "Basic EmonPi Home Energy Monitor configuration", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": { "type": "ProcessArg::FEEDID", "value": "use" } 14 | }, 15 | { 16 | "process": "power_to_kwh", 17 | "arguments": { "type": "ProcessArg::FEEDID", "value": "use_kwh" } 18 | } 19 | ] 20 | } 21 | ], 22 | 23 | "feeds": [ 24 | { 25 | "name": "use", 26 | "type": "DataType::REALTIME", 27 | "engine": "Engine::PHPFINA", 28 | "interval": "10", 29 | "unit": "W" 30 | }, 31 | { 32 | "name": "use_kwh", 33 | "type": "DataType::REALTIME", 34 | "engine": "Engine::PHPFINA", 35 | "interval": "10", 36 | "unit": "kWh" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emontx-HEM.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HomeEnergyMonitor", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonTx", 5 | "description": "Basic EmonTx v3 Home Energy Monitor configuration", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 14 | }, 15 | { 16 | "process": "power_to_kwh", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 18 | } 19 | ] 20 | } 21 | ], 22 | 23 | "feeds": [ 24 | { 25 | "name": "use", 26 | "type": "DataType::REALTIME", 27 | "engine": "Engine::PHPFINA", 28 | "interval": "10", 29 | "unit": "W" 30 | }, 31 | { 32 | "name": "use_kwh", 33 | "type": "DataType::REALTIME", 34 | "engine": "Engine::PHPFINA", 35 | "interval": "10", 36 | "unit": "kWh" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /data/Control/wifirelay.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wi-Fi relay", 3 | "category": "Control", 4 | "group": "MQTT", 5 | "description": "Wi-Fi relay control", 6 | "inputs": [ 7 | { 8 | "name": "period", 9 | "description": "Control period", 10 | "processList": [] 11 | }, 12 | { 13 | "name": "end", 14 | "description": "Control end", 15 | "processList": [] 16 | }, 17 | { 18 | "name": "interruptible", 19 | "description": "Control interruptible", 20 | "processList": [] 21 | }, 22 | { 23 | "name": "status", 24 | "description": "Control status", 25 | "processList": [] 26 | } 27 | ], 28 | 29 | "feeds": [], 30 | 31 | "control": 32 | { 33 | "active": {"name":"Active","type":"checkbox","default":1}, 34 | "period": {"name":"Run period", "type":"time","default":0,"resolution":0.5}, 35 | "end": {"name":"Complete by", "type":"time","default":0,"resolution":0.5}, 36 | "repeat": {"type":"weekly-scheduler","default":[1,1,1,1,1,0,0]}, 37 | "interruptible": {"name":"Ok to interrupt schedule","type":"checkbox","default":0}, 38 | "runonce": {"type":"","default":true}, 39 | "basic": {"type":"","default":0} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /data/Control/smartplug.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Smartplug", 3 | "category": "Control", 4 | "group": "MQTT", 5 | "description": "Smartplug control", 6 | "inputs": [ 7 | { 8 | "name": "period", 9 | "description": "Control period", 10 | "processList": [] 11 | }, 12 | { 13 | "name": "end", 14 | "description": "Control end", 15 | "processList": [] 16 | }, 17 | { 18 | "name": "interruptible", 19 | "description": "Control interruptible", 20 | "processList": [] 21 | }, 22 | { 23 | "name": "status", 24 | "description": "Control status", 25 | "processList": [] 26 | } 27 | ], 28 | 29 | "feeds": [], 30 | 31 | "control": 32 | { 33 | "active": {"name":"Active","type":"checkbox","default":1}, 34 | "period": {"name":"Run period", "type":"time","default":0,"resolution":0.5}, 35 | "end": {"name":"Complete by", "type":"time","default":0,"resolution":0.5}, 36 | "repeat": {"type":"weekly-scheduler","default":[1,1,1,1,1,0,0]}, 37 | "interruptible": {"name":"Ok to interrupt schedule","type":"checkbox","default":0}, 38 | "runonce": {"type":"","default":true}, 39 | "basic": {"type":"","default":0}, 40 | "signal": {"name":"Signal","type":"select","default":"carbonintensity"} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emonth.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EmonTH", 3 | "category": "OpenEnergyMonitor", 4 | "group": "Temperature & Humidity", 5 | "description": "Automatic inputs and feeds creation for emonTH device.", 6 | "inputs": [ 7 | { 8 | "name": "temperature", 9 | "description": "Temperature C", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "emonth_temperature" } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "humidity", 19 | "description": "Humidity Rh%", 20 | "processList": [ 21 | { 22 | "process": "log_to_feed", 23 | "arguments": {"type": "ProcessArg::FEEDID", "value": "emonth_humidity" } 24 | } 25 | ] 26 | } 27 | ], 28 | 29 | "feeds": [ 30 | { 31 | "name": "emonth_temperature", 32 | "type": "DataType::REALTIME", 33 | "engine": "Engine::PHPFINA", 34 | "interval": "60", 35 | "unit": "°C" 36 | }, 37 | { 38 | "name": "emonth_humidity", 39 | "type": "DataType::REALTIME", 40 | "engine": "Engine::PHPFINA", 41 | "interval": "60", 42 | "unit": "%" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Views/device.js: -------------------------------------------------------------------------------- 1 | var device = { 2 | 'list':function() { 3 | var result = {}; 4 | $.ajax({ url: path+"device/list.json", dataType: 'json', async: false, success: function(data) {result = data;} }); 5 | return result; 6 | }, 7 | 8 | 'get':function(id) { 9 | var result = {}; 10 | $.ajax({ url: path+"device/get.json", data: "id="+id, async: false, success: function(data) {result = data;} }); 11 | return result; 12 | }, 13 | 14 | 'set':function(id, fields) { 15 | var result = {}; 16 | $.ajax({ url: path+"device/set.json", data: "id="+id+"&fields="+JSON.stringify(fields), async: false, success: function(data) {result = data;} }); 17 | return result; 18 | }, 19 | 20 | 'setNewDeviceKey':function(id) { 21 | var result = {}; 22 | $.ajax({ url: path+"device/setnewdevicekey.json", data: "id="+id, async: false, success: function(data) {result = data;} }); 23 | return result; 24 | }, 25 | 26 | 'remove':function(id) { 27 | var result = {}; 28 | $.ajax({ url: path+"device/delete.json", data: "id="+id, async: false, success: function(data) {result = data;} }); 29 | return result; 30 | }, 31 | 32 | 'create':function(nodeid, name, description, type) { 33 | var result = {}; 34 | $.ajax({ url: path+"device/create.json", data: "nodeid="+nodeid+"&name="+name+"&description="+description+"&type="+type, async: false, success: function(data) {result = data;} }); 35 | return result; 36 | }, 37 | 38 | 'init':function(id, template) { 39 | var result = {}; 40 | $.ajax({ url: path+"device/init.json?id="+id, type: 'POST', data: "template="+JSON.stringify(template), dataType: 'json', async: false, success: function(data) {result = data;} }); 41 | return result; 42 | }, 43 | 44 | 'prepareTemplate':function(id) { 45 | var result = {}; 46 | $.ajax({ url: path+"device/template/prepare.json", data: "id="+id, dataType: 'json', async: false, success: function(data) {result = data;} }); 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/add_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add_input", 3 | "category": "Examples", 4 | "group": "Simple Functions", 5 | "description": "Show how two inputs can be added", 6 | "inputs": [ 7 | { 8 | "name": "inputA", 9 | "description": "inputA", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": { 14 | "type": "ProcessArg::FEEDID", 15 | "value": "inputA" 16 | } 17 | } 18 | ] 19 | }, 20 | { 21 | "name": "inputB", 22 | "description": "inputB", 23 | "processList": [ 24 | { 25 | "process": "log_to_feed", 26 | "arguments": { 27 | "type": "ProcessArg::FEEDID", 28 | "value": "inputB" 29 | } 30 | }, 31 | { 32 | "process": "add_input", 33 | "arguments": { 34 | "type": "ProcessArg::INPUTID", 35 | "value": "inputA" 36 | } 37 | }, 38 | { 39 | "process": "log_to_feed", 40 | "arguments": { 41 | "type": "ProcessArg::FEEDID", 42 | "value": "total" 43 | } 44 | } 45 | ] 46 | } 47 | ], 48 | "feeds": [ 49 | { 50 | "name": "inputA", 51 | "type": "DataType::REALTIME", 52 | "engine": "Engine::PHPFINA", 53 | "interval": "60" 54 | }, 55 | { 56 | "name": "inputB", 57 | "type": "DataType::REALTIME", 58 | "engine": "Engine::PHPFINA", 59 | "interval": "60" 60 | }, 61 | { 62 | "name": "total", 63 | "type": "DataType::REALTIME", 64 | "engine": "Engine::PHPFINA", 65 | "interval": "60" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /examples/reset2original.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reset2original", 3 | "category": "Examples", 4 | "group": "Simple Functions", 5 | "description": "Show how the reset gives the original value to an input to a feed", 6 | "inputs": [ 7 | { 8 | "name": "inputA", 9 | "description": "inputA", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": { 14 | "type": "ProcessArg::FEEDID", 15 | "value": "inputA" 16 | } 17 | } 18 | ] 19 | }, 20 | { 21 | "name": "inputB", 22 | "description": "inputB", 23 | "processList": [ 24 | { 25 | "process": "log_to_feed", 26 | "arguments": { 27 | "type": "ProcessArg::FEEDID", 28 | "value": "inputB" 29 | } 30 | }, 31 | { 32 | "process": "add_input", 33 | "arguments": { 34 | "type": "ProcessArg::INPUTID", 35 | "value": "inputA" 36 | } 37 | }, 38 | { 39 | "process": "log_to_feed", 40 | "arguments": { 41 | "type": "ProcessArg::FEEDID", 42 | "value": "total" 43 | } 44 | }, 45 | { 46 | "process": "reset2original", 47 | "arguments": { 48 | "type": "ProcessArg::NONE" 49 | } 50 | }, 51 | { 52 | "process": "log_to_feed", 53 | "arguments": { 54 | "type": "ProcessArg::FEEDID", 55 | "value": "resetOfInputB" 56 | } 57 | } 58 | ] 59 | } 60 | ], 61 | "feeds": [ 62 | { 63 | "name": "inputA", 64 | "type": "DataType::REALTIME", 65 | "engine": "Engine::PHPFINA", 66 | "interval": "60" 67 | }, 68 | { 69 | "name": "inputB", 70 | "type": "DataType::REALTIME", 71 | "engine": "Engine::PHPFINA", 72 | "interval": "60" 73 | }, 74 | { 75 | "name": "total", 76 | "type": "DataType::REALTIME", 77 | "engine": "Engine::PHPFINA", 78 | "interval": "60" 79 | }, 80 | { 81 | "name": "resetOfInputB", 82 | "type": "DataType::REALTIME", 83 | "engine": "Engine::PHPFINA", 84 | "interval": "60" 85 | } 86 | ] 87 | } -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emonpi-SPV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solar PV Type 2", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonPi", 5 | "description": "EmonPi Solar PV Type 2 template", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "allowpositive", 13 | "arguments": {"type": "ProcessArg::NONE"} 14 | }, 15 | { 16 | "process": "log_to_feed", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import" } 18 | }, 19 | { 20 | "process": "power_to_kwh", 21 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import_kwh" } 22 | } 23 | ] 24 | }, 25 | { 26 | "name": "power2", 27 | "description": "Solar generation", 28 | "processList": [ 29 | { 30 | "process": "log_to_feed", 31 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 32 | }, 33 | { 34 | "process": "power_to_kwh", 35 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 36 | }, 37 | { 38 | "process": "add_input", 39 | "arguments": {"type": "ProcessArg::INPUTID", "value": "power1" } 40 | }, 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 44 | }, 45 | { 46 | "process": "power_to_kwh", 47 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 48 | } 49 | ] 50 | } 51 | ], 52 | 53 | "feeds": [ 54 | { 55 | "name": "use", 56 | "type": "DataType::REALTIME", 57 | "engine": "Engine::PHPFINA", 58 | "interval": "10", 59 | "unit": "W" 60 | }, 61 | { 62 | "name": "use_kwh", 63 | "type": "DataType::REALTIME", 64 | "engine": "Engine::PHPFINA", 65 | "interval": "10", 66 | "unit": "kWh" 67 | }, 68 | { 69 | "name": "solar", 70 | "type": "DataType::REALTIME", 71 | "engine": "Engine::PHPFINA", 72 | "interval": "10", 73 | "unit": "W" 74 | }, 75 | { 76 | "name": "solar_kwh", 77 | "type": "DataType::REALTIME", 78 | "engine": "Engine::PHPFINA", 79 | "interval": "10", 80 | "unit": "kWh" 81 | }, 82 | { 83 | "name": "import", 84 | "type": "DataType::REALTIME", 85 | "engine": "Engine::PHPFINA", 86 | "interval": "10", 87 | "unit": "W" 88 | }, 89 | { 90 | "name": "import_kwh", 91 | "type": "DataType::REALTIME", 92 | "engine": "Engine::PHPFINA", 93 | "interval": "10", 94 | "unit": "kWh" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emonpi-SPV1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solar PV Type 1", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonPi", 5 | "description": "EmonPi Solar PV Type 1 template", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 14 | }, 15 | { 16 | "process": "power_to_kwh", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 18 | }, 19 | { 20 | "process": "subtract_input", 21 | "arguments": {"type": "ProcessArg::INPUTID", "value": "power2" } 22 | }, 23 | { 24 | "process": "allowpositive", 25 | "arguments": {"type": "ProcessArg::NONE"} 26 | }, 27 | { 28 | "process": "log_to_feed", 29 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import" } 30 | }, 31 | { 32 | "process": "power_to_kwh", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import_kwh" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "power2", 39 | "description": "Solar generation", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 44 | }, 45 | { 46 | "process": "power_to_kwh", 47 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 48 | } 49 | ] 50 | } 51 | ], 52 | 53 | "feeds": [ 54 | { 55 | "name": "use", 56 | "type": "DataType::REALTIME", 57 | "engine": "Engine::PHPFINA", 58 | "interval": "10", 59 | "unit": "W" 60 | }, 61 | { 62 | "name": "use_kwh", 63 | "type": "DataType::REALTIME", 64 | "engine": "Engine::PHPFINA", 65 | "interval": "10", 66 | "unit": "kWh" 67 | }, 68 | { 69 | "name": "solar", 70 | "type": "DataType::REALTIME", 71 | "engine": "Engine::PHPFINA", 72 | "interval": "10", 73 | "unit": "W" 74 | }, 75 | { 76 | "name": "solar_kwh", 77 | "type": "DataType::REALTIME", 78 | "engine": "Engine::PHPFINA", 79 | "interval": "10", 80 | "unit": "kWh" 81 | }, 82 | { 83 | "name": "import", 84 | "type": "DataType::REALTIME", 85 | "engine": "Engine::PHPFINA", 86 | "interval": "10", 87 | "unit": "W" 88 | }, 89 | { 90 | "name": "import_kwh", 91 | "type": "DataType::REALTIME", 92 | "engine": "Engine::PHPFINA", 93 | "interval": "10", 94 | "unit": "kWh" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emontx-SPV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solar PV Type 2", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonTx", 5 | "description": "EmonTx Solar PV Type 2 template using CT4 for solar", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "allowpositive", 13 | "arguments": {"type": "ProcessArg::NONE"} 14 | }, 15 | { 16 | "process": "log_to_feed", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import" } 18 | }, 19 | { 20 | "process": "power_to_kwh", 21 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import_kwh" } 22 | } 23 | ] 24 | }, 25 | { 26 | "name": "power4", 27 | "description": "Solar generation", 28 | "processList": [ 29 | { 30 | "process": "log_to_feed", 31 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 32 | }, 33 | { 34 | "process": "power_to_kwh", 35 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 36 | }, 37 | { 38 | "process": "add_input", 39 | "arguments": {"type": "ProcessArg::INPUTID", "value": "power1" } 40 | }, 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 44 | }, 45 | { 46 | "process": "power_to_kwh", 47 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 48 | } 49 | ] 50 | } 51 | ], 52 | 53 | "feeds": [ 54 | { 55 | "name": "use", 56 | "type": "DataType::REALTIME", 57 | "engine": "Engine::PHPFINA", 58 | "interval": "10", 59 | "unit": "W" 60 | }, 61 | { 62 | "name": "use_kwh", 63 | "type": "DataType::REALTIME", 64 | "engine": "Engine::PHPFINA", 65 | "interval": "10", 66 | "unit": "kWh" 67 | }, 68 | { 69 | "name": "solar", 70 | "type": "DataType::REALTIME", 71 | "engine": "Engine::PHPFINA", 72 | "interval": "10", 73 | "unit": "W" 74 | }, 75 | { 76 | "name": "solar_kwh", 77 | "type": "DataType::REALTIME", 78 | "engine": "Engine::PHPFINA", 79 | "interval": "10", 80 | "unit": "kWh" 81 | }, 82 | { 83 | "name": "import", 84 | "type": "DataType::REALTIME", 85 | "engine": "Engine::PHPFINA", 86 | "interval": "10", 87 | "unit": "W" 88 | }, 89 | { 90 | "name": "import_kwh", 91 | "type": "DataType::REALTIME", 92 | "engine": "Engine::PHPFINA", 93 | "interval": "10", 94 | "unit": "kWh" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /data/OpenEnergyMonitor/emontx-SPV1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solar PV Type 1", 3 | "category": "OpenEnergyMonitor", 4 | "group": "EmonTx", 5 | "description": "EmonTx Solar PV Type 1 template using CT4 for solar", 6 | "inputs": [ 7 | { 8 | "name": "power1", 9 | "description": "House consumption", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 14 | }, 15 | { 16 | "process": "power_to_kwh", 17 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 18 | }, 19 | { 20 | "process": "subtract_input", 21 | "arguments": {"type": "ProcessArg::INPUTID", "value": "power4" } 22 | }, 23 | { 24 | "process": "allowpositive", 25 | "arguments": {"type": "ProcessArg::NONE"} 26 | }, 27 | { 28 | "process": "log_to_feed", 29 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import" } 30 | }, 31 | { 32 | "process": "power_to_kwh", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import_kwh" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "power4", 39 | "description": "Solar generation", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 44 | }, 45 | { 46 | "process": "power_to_kwh", 47 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 48 | } 49 | ] 50 | } 51 | ], 52 | 53 | "feeds": [ 54 | { 55 | "name": "use", 56 | "type": "DataType::REALTIME", 57 | "engine": "Engine::PHPFINA", 58 | "interval": "10", 59 | "unit": "W" 60 | }, 61 | { 62 | "name": "use_kwh", 63 | "type": "DataType::REALTIME", 64 | "engine": "Engine::PHPFINA", 65 | "interval": "10", 66 | "unit": "kWh" 67 | }, 68 | { 69 | "name": "solar", 70 | "type": "DataType::REALTIME", 71 | "engine": "Engine::PHPFINA", 72 | "interval": "10", 73 | "unit": "W" 74 | }, 75 | { 76 | "name": "solar_kwh", 77 | "type": "DataType::REALTIME", 78 | "engine": "Engine::PHPFINA", 79 | "interval": "10", 80 | "unit": "kWh" 81 | }, 82 | { 83 | "name": "import", 84 | "type": "DataType::REALTIME", 85 | "engine": "Engine::PHPFINA", 86 | "interval": "10", 87 | "unit": "W" 88 | }, 89 | { 90 | "name": "import_kwh", 91 | "type": "DataType::REALTIME", 92 | "engine": "Engine::PHPFINA", 93 | "interval": "10", 94 | "unit": "kWh" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emoncms Device module 2 | 3 | ![3.png](files/3.png) 4 | 5 | - Auto-configuration of inputs and feeds from pre-defined device templates 6 | - Device templates available for all standard OpenEnergyMonitor hardware units 7 | - Device level access keys 8 | 9 | Original author: [Chaveiro](https://github.com/chaveiro/) 10 | Contributions from: [Trystan Lea](http://github.com/trystanlea), [Adminde](https://github.com/adminde) 11 | 12 | ## Installation 13 | 14 | The following steps document the installation of the device module on a stock emonpi/emonbase running the latest emonSD image. 15 | 16 | After logging in via SSH, place the pi in write mode: 17 | 18 | rpi-rw 19 | 20 | Navigate to the emoncms Modules folder: 21 | 22 | cd /var/www/emoncms/Modules 23 | 24 | Clone the device module into the modules folder using git: 25 | 26 | git clone https://github.com/emoncms/device.git 27 | 28 | Login to emoncms on your emonpi/emonbase, navigate to Setup > Administration, Update the emoncms database by running 'Update & Check' under the Update database section. 29 | 30 | ## Using the device module 31 | 32 | **Auto-configration** 33 | 34 | One of the useful features of the device module is automatic configuration of inputs and feeds according to pre-defined device templates. 35 | 36 | The following is an example of automatic configuration of an emonpi for use as a Type 1 Solar PV monitor. 37 | 38 | With the EmonPi and CT sensors connected up the emoncms inputs list running on the EmonPi should show a list of inputs like so: 39 | 40 | ![1.png](files/1.png) 41 | 42 | These are unconfigured at this point and no data is being recorded. The manual setup for a Type 1 Solar PV setup is documented here [OpenEnergyMonitor Guide: SolarPV](https://guide.openenergymonitor.org/applications/solar-pv), but we are going to use the device module here to automatically setup the inputs and feeds. 43 | 44 | Navigate to the device module, click on Setup > Device Setup, which should bring up the following page: 45 | 46 | ![2.png](files/2.png) 47 | 48 | Click on 'New device', this will bring up the 'Configure Device' window. The left-hand pane lists the device templates available. 49 | 50 | Click on OpenEnergyMonitor > EmonPi > Solar PV Type 1 to select this template. 51 | 52 | Enter the nodename emonpi in both the 'Node' and 'Name' fields, set a location as you wish or leave blank. 53 | 54 | Click save to continue. 55 | 56 | ![3.png](files/3.png) 57 | 58 | When a new device is created, it will automatically be initialized and all input processes and feeds will be created, according to the pre-defined template. If the template needs to be applied again, as e.g. a process list was experimented on and altered, or single feeds deleted, it may be re-initialized. Clicking the cycling arrows 'refresh icon' will bring up the initialization window. 59 | 60 | ![4.png](files/4.png) 61 | 62 | Click 'Initialize' to re-initialize the device and to confirm the creation of missing feeds and inputs, as well as the reset of configured processes to their original state. 63 | 64 | ![5.png](files/5.png) 65 | 66 | The input list will now show the input processes created: 67 | 68 | ![6.png](files/6.png) 69 | 70 | and the feeds page the feeds created: 71 | 72 | ![7.png](files/7.png) 73 | 74 | ## Development 75 | 76 | See: [Development: Devices, Inputs and Feeds in emoncms](https://community.openenergymonitor.org/t/development-devices-inputs-and-feeds-in-emoncms/4281/17) 77 | -------------------------------------------------------------------------------- /data/CircuitSetup/circuitsetup_split-phase.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Split Single Phase Meter", 3 | "category": "CircuitSetup", 4 | "group": "Energy Meters", 5 | "description": "CircuitSetup Split Single Phase Energy Meter", 6 | "inputs": [ 7 | { 8 | "name": "V1", 9 | "description": "Voltage 1", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 1" } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "V2", 19 | "description": "Voltage 2", 20 | "processList": [ 21 | { 22 | "process": "log_to_feed", 23 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 2" } 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "totV", 29 | "description": "Total Voltage", 30 | "processList": [ 31 | { 32 | "process": "log_to_feed", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Voltage" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "CT1", 39 | "description": "Current 1", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 1" } 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "CT2", 49 | "description": "Current 2", 50 | "processList": [ 51 | { 52 | "process": "log_to_feed", 53 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 2" } 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "totI", 59 | "description": "Total Current", 60 | "processList": [ 61 | { 62 | "process": "log_to_feed", 63 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Current" } 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "PF", 69 | "description": "Power Factor", 70 | "processList": [ 71 | { 72 | "process": "log_to_feed", 73 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Power Factor" } 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "temp", 79 | "description": "Temp", 80 | "processList": [ 81 | { 82 | "process": "log_to_feed", 83 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Temp" } 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "freq", 89 | "description": "Frequency", 90 | "processList": [ 91 | { 92 | "process": "log_to_feed", 93 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Frequency" } 94 | } 95 | ] 96 | }, 97 | { 98 | "name": "W", 99 | "description": "Total Watts", 100 | "processList": [ 101 | { 102 | "process": "log_to_feed", 103 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Watts" } 104 | }, 105 | { 106 | "process": "power_to_kwh", 107 | "arguments": {"type": "ProcessArg::FEEDID", "value": "kWh" } 108 | } 109 | ] 110 | } 111 | ], 112 | 113 | "feeds": [ 114 | { 115 | "name": "Voltage 1", 116 | "type": "DataType::REALTIME", 117 | "engine": "Engine::PHPFINA", 118 | "interval": "10", 119 | "unit": "V" 120 | }, 121 | { 122 | "name": "Voltage 2", 123 | "type": "DataType::REALTIME", 124 | "engine": "Engine::PHPFINA", 125 | "interval": "10", 126 | "unit": "V" 127 | }, 128 | { 129 | "name": "Total Voltage", 130 | "type": "DataType::REALTIME", 131 | "engine": "Engine::PHPFINA", 132 | "interval": "10", 133 | "unit": "V" 134 | }, 135 | { 136 | "name": "Current 1", 137 | "type": "DataType::REALTIME", 138 | "engine": "Engine::PHPFINA", 139 | "interval": "10", 140 | "unit": "A" 141 | }, 142 | { 143 | "name": "Current 2", 144 | "type": "DataType::REALTIME", 145 | "engine": "Engine::PHPFINA", 146 | "interval": "10", 147 | "unit": "A" 148 | }, 149 | { 150 | "name": "Total Current", 151 | "type": "DataType::REALTIME", 152 | "engine": "Engine::PHPFINA", 153 | "interval": "10", 154 | "unit": "A" 155 | }, 156 | { 157 | "name": "Total Watts", 158 | "type": "DataType::REALTIME", 159 | "engine": "Engine::PHPFINA", 160 | "interval": "10", 161 | "unit": "W" 162 | }, 163 | { 164 | "name": "kWh", 165 | "type": "DataType::REALTIME", 166 | "engine": "Engine::PHPFINA", 167 | "interval": "10", 168 | "unit": "kWh" 169 | }, 170 | { 171 | "name": "Power Factor", 172 | "type": "DataType::REALTIME", 173 | "engine": "Engine::PHPFINA", 174 | "interval": "10", 175 | "unit": "" 176 | }, 177 | { 178 | "name": "Temp", 179 | "type": "DataType::REALTIME", 180 | "engine": "Engine::PHPFINA", 181 | "interval": "10", 182 | "unit": "°C" 183 | }, 184 | { 185 | "name": "Frequency", 186 | "type": "DataType::REALTIME", 187 | "engine": "Engine::PHPFINA", 188 | "interval": "10", 189 | "unit": "" 190 | } 191 | ] 192 | } 193 | -------------------------------------------------------------------------------- /data/OpenEVSE/openevse.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default", 3 | "category": "EVSE", 4 | "group": "OpenEVSE", 5 | "description": "OpenEVSE / EmonEVSE auto configuration", 6 | "inputs": [ 7 | { 8 | "name": "amp", 9 | "description": "Real-time charging current", 10 | "processList": [ 11 | { 12 | "process": "scale", 13 | "arguments": { "type": "ProcessArg::VALUE", "value": 0.001 } 14 | }, 15 | { 16 | "process": "log_to_feed", 17 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_current" } 18 | }, 19 | { 20 | "process": "scale", 21 | "arguments": { "type": "ProcessArg::VALUE", "value": 230 } 22 | }, 23 | { 24 | "process": "log_to_feed", 25 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_power" } 26 | } 27 | 28 | 29 | ] 30 | }, 31 | { 32 | "name": "wh", 33 | "description": "Cumulative Energy", 34 | "processList": [ 35 | { 36 | "process": "scale", 37 | "arguments": { "type": "ProcessArg::VALUE", "value": 0.001 } 38 | }, 39 | { 40 | "process": "wh_accumulator", 41 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_energy" } 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "temp1", 47 | "description": "EVSE internal temperature", 48 | "processList": [ 49 | { 50 | "process": "scale", 51 | "arguments": { "type": "ProcessArg::VALUE", "value": 0.1 } 52 | }, 53 | { 54 | "process": "log_to_feed", 55 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_temperature" } 56 | } 57 | ] 58 | }, 59 | { 60 | "name": "state", 61 | "description": "State", 62 | "processList": [ 63 | { 64 | "process": "log_to_feed", 65 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_state" } 66 | } 67 | ] 68 | }, 69 | { 70 | "name": "pilot", 71 | "description": "Pilot", 72 | "processList": [ 73 | { 74 | "process": "log_to_feed", 75 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_pilot" } 76 | } 77 | ] 78 | }, 79 | { 80 | "name": "divertmode", 81 | "description": "Divert Mode state", 82 | "processList": [ 83 | { 84 | "process": "log_to_feed", 85 | "arguments": { "type": "ProcessArg::FEEDID", "value": "ev_divertmode_state" } 86 | } 87 | ] 88 | } 89 | ], 90 | 91 | "feeds": [ 92 | { 93 | "name": "ev_current", 94 | "type": "DataType::REALTIME", 95 | "engine": "Engine::PHPFINA", 96 | "interval": "30", 97 | "unit": "A" 98 | }, 99 | { 100 | "name": "ev_power", 101 | "type": "DataType::REALTIME", 102 | "engine": "Engine::PHPFINA", 103 | "interval": "30", 104 | "unit": "W" 105 | }, 106 | { 107 | "name": "ev_energy", 108 | "type": "DataType::REALTIME", 109 | "engine": "Engine::PHPFINA", 110 | "interval": "30", 111 | "unit": "kWh" 112 | }, 113 | { 114 | "name": "ev_temperature", 115 | "type": "DataType::REALTIME", 116 | "engine": "Engine::PHPFINA", 117 | "interval": "30", 118 | "unit": "°C" 119 | }, 120 | { 121 | "name": "ev_state", 122 | "type": "DataType::REALTIME", 123 | "engine": "Engine::PHPFINA", 124 | "interval": "30", 125 | "unit": "" 126 | }, 127 | { 128 | "name": "ev_pilot", 129 | "type": "DataType::REALTIME", 130 | "engine": "Engine::PHPFINA", 131 | "interval": "30", 132 | "unit": "A" 133 | }, 134 | { 135 | "name": "ev_divertmode_state", 136 | "type": "DataType::REALTIME", 137 | "engine": "Engine::PHPFINA", 138 | "interval": "30", 139 | "unit": "" 140 | } 141 | ], 142 | 143 | "control": 144 | { 145 | "active": {"name":"Active","type":"checkbox","default":1}, 146 | "period": {"name":"Run period", "type":"time","default":0,"resolution":0.5}, 147 | "end": {"name":"Complete by", "type":"time","default":0,"resolution":0.5}, 148 | "repeat": {"type":"weekly-scheduler","default":[1,1,1,1,1,0,0]}, 149 | "interruptible": {"name":"Ok to interrupt schedule","type":"checkbox","default":0}, 150 | "runonce": {"type":"","default":true}, 151 | "basic": {"type":"","default":0}, 152 | "signal": {"name":"Signal","type":"select","default":"carbonintensity"} 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Views/device_api.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 |

13 |

14 |

15 | 18 |


19 | 20 |

21 |


22 | 23 |

24 | 25 |

26 |

27 |

28 | 30 |

31 | 32 |

33 | 34 | 35 | 36 |
device/view
device/api
37 | 38 |

39 |

.json'); ?>

40 | 41 |

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
device/list.json
device/get.json?id=1
device/create.json?nodeid=Test&name=Test
device/delete.json?id=1
device/set.json?id=1&fields={"name":"Test","description":"Room","nodeid":"House","type":"test"}
device/init.json?id=1
50 | 51 |

52 | 53 | 54 | 55 | 56 |
device/auth/request.json
device/auth/check.json
device/auth/allow.json?ip=127.0.0.1
57 | 58 |

59 | 60 | 61 | 62 | 63 | 64 | 65 |
device/template/listshort.json
device/template/list.json
device/template/reload.json
device/template/get.json?type=example
device/template/prepare.json?id=1
66 | 67 | 68 |

69 |

\'\\Modules\\device\\data\\*.json\''); ?>

70 |

71 |

72 | -------------------------------------------------------------------------------- /data/amrm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AMRM", 3 | "category": "Datalogger", 4 | "group": "ArchiMetrics", 5 | "description": "INPUTS & FEEDS for AMRM Logger from archimetrics.co.uk", 6 | "inputs": [{ 7 | "name": "n1T", 8 | "description": "N1 Temperature", 9 | "processList": [{ 10 | "process": "log_to_feed", 11 | "arguments": { 12 | "type": "ProcessArg::FEEDID", 13 | "value": "n1T" 14 | } 15 | }] 16 | }, 17 | { 18 | "name": "n2T", 19 | "description": "N2 Temperature", 20 | "processList": [{ 21 | "process": "log_to_feed", 22 | "arguments": { 23 | "type": "ProcessArg::FEEDID", 24 | "value": "n2T" 25 | } 26 | }] 27 | }, 28 | { 29 | "name": "n3T", 30 | "description": "N3 Temperature", 31 | "processList": [{ 32 | "process": "log_to_feed", 33 | "arguments": { 34 | "type": "ProcessArg::FEEDID", 35 | "value": "n3T" 36 | } 37 | }] 38 | }, 39 | { 40 | "name": "n4T", 41 | "description": "N4 Temperature", 42 | "processList": [{ 43 | "process": "log_to_feed", 44 | "arguments": { 45 | "type": "ProcessArg::FEEDID", 46 | "value": "n4T" 47 | } 48 | }] 49 | }, 50 | { 51 | "name": "n1IM", 52 | "description": "N1 IM", 53 | "processList": [{ 54 | "process": "log_to_feed", 55 | "arguments": { 56 | "type": "ProcessArg::FEEDID", 57 | "value": "n1IM" 58 | } 59 | }] 60 | }, 61 | { 62 | "name": "n2IM", 63 | "description": "N2 IM", 64 | "processList": [{ 65 | "process": "log_to_feed", 66 | "arguments": { 67 | "type": "ProcessArg::FEEDID", 68 | "value": "n2IM" 69 | } 70 | }] 71 | }, 72 | { 73 | "name": "n3IM", 74 | "description": "N3 IM", 75 | "processList": [{ 76 | "process": "log_to_feed", 77 | "arguments": { 78 | "type": "ProcessArg::FEEDID", 79 | "value": "n3IM" 80 | } 81 | }] 82 | }, 83 | { 84 | "name": "n4IM", 85 | "description": "N4 IM", 86 | "processList": [{ 87 | "process": "log_to_feed", 88 | "arguments": { 89 | "type": "ProcessArg::FEEDID", 90 | "value": "n4IM" 91 | } 92 | }] 93 | }, 94 | { 95 | "name": "n1DM", 96 | "description": "N1 DM", 97 | "processList": [{ 98 | "process": "log_to_feed", 99 | "arguments": { 100 | "type": "ProcessArg::FEEDID", 101 | "value": "n1DM" 102 | } 103 | }] 104 | }, 105 | { 106 | "name": "n2DM", 107 | "description": "N2 DM", 108 | "processList": [{ 109 | "process": "log_to_feed", 110 | "arguments": { 111 | "type": "ProcessArg::FEEDID", 112 | "value": "n2DM" 113 | } 114 | }] 115 | }, 116 | { 117 | "name": "n3DM", 118 | "description": "N3 DM", 119 | "processList": [{ 120 | "process": "log_to_feed", 121 | "arguments": { 122 | "type": "ProcessArg::FEEDID", 123 | "value": "n3DM" 124 | } 125 | }] 126 | }, 127 | { 128 | "name": "n4DM", 129 | "description": "N4 DM", 130 | "processList": [{ 131 | "process": "log_to_feed", 132 | "arguments": { 133 | "type": "ProcessArg::FEEDID", 134 | "value": "n4DM" 135 | } 136 | }] 137 | }, 138 | { 139 | "name": "iTA", 140 | "description": "Internal Temperature Air", 141 | "processList": [{ 142 | "process": "log_to_feed", 143 | "arguments": { 144 | "type": "ProcessArg::FEEDID", 145 | "value": "iTA" 146 | } 147 | }] 148 | }, 149 | { 150 | "name": "iRH", 151 | "description": "Internal RH", 152 | "processList": [{ 153 | "process": "log_to_feed", 154 | "arguments": { 155 | "type": "ProcessArg::FEEDID", 156 | "value": "iRH" 157 | } 158 | }] 159 | }, 160 | { 161 | "name": "iDP", 162 | "description": "Internal Dew Point", 163 | "processList": [{ 164 | "process": "log_to_feed", 165 | "arguments": { 166 | "type": "ProcessArg::FEEDID", 167 | "value": "iDP" 168 | } 169 | }] 170 | }, 171 | { 172 | "name": "SYS", 173 | "description": "System Temperature", 174 | "processList": [{ 175 | "process": "log_to_feed", 176 | "arguments": { 177 | "type": "ProcessArg::FEEDID", 178 | "value": "SYS" 179 | } 180 | }] 181 | }], 182 | "feeds": [{ 183 | "name": "n1T", 184 | "type": "DataType::REALTIME", 185 | "engine": "Engine::PHPFINA", 186 | "interval": "10" 187 | }, 188 | { 189 | "name": "n2T", 190 | "type": "DataType::REALTIME", 191 | "engine": "Engine::PHPFINA", 192 | "interval": "10" 193 | 194 | }, 195 | { 196 | "name": "n3T", 197 | "type": "DataType::REALTIME", 198 | "engine": "Engine::PHPFINA", 199 | "interval": "10" 200 | }, 201 | { 202 | "name": "n4T", 203 | "type": "DataType::REALTIME", 204 | "engine": "Engine::PHPFINA", 205 | "interval": "10" 206 | }, 207 | { 208 | "name": "n1IM", 209 | "type": "DataType::REALTIME", 210 | "engine": "Engine::PHPFINA", 211 | "interval": "10" 212 | }, 213 | { 214 | "name": "n2IM", 215 | "type": "DataType::REALTIME", 216 | "engine": "Engine::PHPFINA", 217 | "interval": "10" 218 | }, 219 | { 220 | "name": "n3IM", 221 | "type": "DataType::REALTIME", 222 | "engine": "Engine::PHPFINA", 223 | "interval": "10" 224 | }, 225 | { 226 | "name": "n4IM", 227 | "type": "DataType::REALTIME", 228 | "engine": "Engine::PHPFINA", 229 | "interval": "10" 230 | }, 231 | { 232 | "name": "n1DM", 233 | "type": "DataType::REALTIME", 234 | "engine": "Engine::PHPFINA", 235 | "interval": "10" 236 | }, 237 | { 238 | "name": "n2DM", 239 | "type": "DataType::REALTIME", 240 | "engine": "Engine::PHPFINA", 241 | "interval": "10" 242 | }, 243 | { 244 | "name": "n3DM", 245 | "type": "DataType::REALTIME", 246 | "engine": "Engine::PHPFINA", 247 | "interval": "10" 248 | }, 249 | { 250 | "name": "n4DM", 251 | "type": "DataType::REALTIME", 252 | "engine": "Engine::PHPFINA", 253 | "interval": "10" 254 | }, 255 | { 256 | "name": "iTA", 257 | "type": "DataType::REALTIME", 258 | "engine": "Engine::PHPFINA", 259 | "interval": "10" 260 | }, 261 | { 262 | "name": "iRH", 263 | "type": "DataType::REALTIME", 264 | "engine": "Engine::PHPFINA", 265 | "interval": "10" 266 | }, 267 | { 268 | "name": "iDP", 269 | "type": "DataType::REALTIME", 270 | "engine": "Engine::PHPFINA", 271 | "interval": "10" 272 | }, 273 | { 274 | "name": "SYS", 275 | "type": "DataType::REALTIME", 276 | "engine": "Engine::PHPFINA", 277 | "interval": "10" 278 | }] 279 | } 280 | -------------------------------------------------------------------------------- /data/CircuitSetup/circuitsetup-6_channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6 Channel Meter", 3 | "category": "CircuitSetup", 4 | "group": "Energy Meters", 5 | "description": "CircuitSetup 6 Channel", 6 | "inputs": [ 7 | { 8 | "name": "V1", 9 | "description": "Voltage 1", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 1" } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "V2", 19 | "description": "Voltage 2", 20 | "processList": [ 21 | { 22 | "process": "log_to_feed", 23 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 2" } 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "CT1", 29 | "description": "Current 1", 30 | "processList": [ 31 | { 32 | "process": "log_to_feed", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 1" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "CT2", 39 | "description": "Current 2", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 2" } 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "CT3", 49 | "description": "Current 3", 50 | "processList": [ 51 | { 52 | "process": "log_to_feed", 53 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 3" } 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "CT4", 59 | "description": "Current 4", 60 | "processList": [ 61 | { 62 | "process": "log_to_feed", 63 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 4" } 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "CT5", 69 | "description": "Current 5", 70 | "processList": [ 71 | { 72 | "process": "log_to_feed", 73 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 5" } 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "CT6", 79 | "description": "Current 6", 80 | "processList": [ 81 | { 82 | "process": "log_to_feed", 83 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 6" } 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "PF1", 89 | "description": "Power Factor 1", 90 | "processList": [ 91 | { 92 | "process": "log_to_feed", 93 | "arguments": {"type": "ProcessArg::FEEDID", "value": "PF 1" } 94 | } 95 | ] 96 | }, 97 | { 98 | "name": "PF2", 99 | "description": "Power Factor 2", 100 | "processList": [ 101 | { 102 | "process": "log_to_feed", 103 | "arguments": {"type": "ProcessArg::FEEDID", "value": "PF 2" } 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "temp", 109 | "description": "Temp", 110 | "processList": [ 111 | { 112 | "process": "log_to_feed", 113 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Temp" } 114 | } 115 | ] 116 | }, 117 | { 118 | "name": "freq", 119 | "description": "Frequency", 120 | "processList": [ 121 | { 122 | "process": "log_to_feed", 123 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Frequency" } 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "W1", 129 | "description": "Total Power", 130 | "processList": [ 131 | { 132 | "process": "add_input", 133 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W2" } 134 | }, 135 | { 136 | "process": "add_input", 137 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W3" } 138 | }, 139 | { 140 | "process": "add_input", 141 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W4" } 142 | }, 143 | { 144 | "process": "add_input", 145 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W5" } 146 | }, 147 | { 148 | "process": "add_input", 149 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W6" } 150 | }, 151 | { 152 | "process": "log_to_feed", 153 | "arguments": {"type": "ProcessArg::FEEDID", "value": "total_power" } 154 | }, 155 | { 156 | "process": "power_to_kwh", 157 | "arguments": {"type": "ProcessArg::FEEDID", "value": "total_power_kwh" } 158 | } 159 | ] 160 | } 161 | ], 162 | 163 | "feeds": [ 164 | { 165 | "name": "Voltage 1", 166 | "type": "DataType::REALTIME", 167 | "engine": "Engine::PHPFINA", 168 | "interval": "10", 169 | "unit": "V" 170 | }, 171 | { 172 | "name": "Voltage 2", 173 | "type": "DataType::REALTIME", 174 | "engine": "Engine::PHPFINA", 175 | "interval": "10", 176 | "unit": "V" 177 | }, 178 | { 179 | "name": "Current 1", 180 | "type": "DataType::REALTIME", 181 | "engine": "Engine::PHPFINA", 182 | "interval": "10", 183 | "unit": "A" 184 | }, 185 | { 186 | "name": "Current 2", 187 | "type": "DataType::REALTIME", 188 | "engine": "Engine::PHPFINA", 189 | "interval": "10", 190 | "unit": "A" 191 | }, 192 | { 193 | "name": "Current 3", 194 | "type": "DataType::REALTIME", 195 | "engine": "Engine::PHPFINA", 196 | "interval": "10", 197 | "unit": "A" 198 | }, 199 | { 200 | "name": "Current 4", 201 | "type": "DataType::REALTIME", 202 | "engine": "Engine::PHPFINA", 203 | "interval": "10", 204 | "unit": "A" 205 | }, 206 | { 207 | "name": "Current 5", 208 | "type": "DataType::REALTIME", 209 | "engine": "Engine::PHPFINA", 210 | "interval": "10", 211 | "unit": "A" 212 | }, 213 | { 214 | "name": "Current 6", 215 | "type": "DataType::REALTIME", 216 | "engine": "Engine::PHPFINA", 217 | "interval": "10", 218 | "unit": "A" 219 | }, 220 | { 221 | "name": "total_power", 222 | "type": "DataType::REALTIME", 223 | "engine": "Engine::PHPFINA", 224 | "interval": "10", 225 | "unit": "W" 226 | }, 227 | { 228 | "name": "total_power_kwh", 229 | "type": "DataType::REALTIME", 230 | "engine": "Engine::PHPFINA", 231 | "interval": "10", 232 | "unit": "kWh" 233 | }, 234 | { 235 | "name": "PF 1", 236 | "type": "DataType::REALTIME", 237 | "engine": "Engine::PHPFINA", 238 | "interval": "10", 239 | "unit": "" 240 | }, 241 | { 242 | "name": "PF 2", 243 | "type": "DataType::REALTIME", 244 | "engine": "Engine::PHPFINA", 245 | "interval": "10", 246 | "unit": "" 247 | }, 248 | { 249 | "name": "Temp", 250 | "type": "DataType::REALTIME", 251 | "engine": "Engine::PHPFINA", 252 | "interval": "10", 253 | "unit": "°C" 254 | }, 255 | { 256 | "name": "Frequency", 257 | "type": "DataType::REALTIME", 258 | "engine": "Engine::PHPFINA", 259 | "interval": "10", 260 | "unit": "Hz" 261 | } 262 | ] 263 | } 264 | -------------------------------------------------------------------------------- /device_controller.php: -------------------------------------------------------------------------------- 1 | format == 'html') 16 | { 17 | if ($route->action == "view" && $session['write']) { 18 | $templates = $device->get_template_list_meta($session['userid']); 19 | $result = view("Modules/device/Views/device_view.php", array('templates'=>$templates)); 20 | } 21 | else if ($route->action == 'api') $result = view("Modules/device/Views/device_api.php", array()); 22 | } 23 | 24 | if ($route->format == 'json') 25 | { 26 | // --------------------------------------------------------------- 27 | // Method for sharing authentication details with a node 28 | // that does not require copying and pasting passwords and apikeys 29 | // 1. device requests authentication - reply "request registered" 30 | // 2. notification asks user whether to allow or deny device 31 | // 3. user clicks on allow 32 | // 4. device makes follow up request for authentication 33 | // - reply authentication details 34 | // --------------------------------------------------------------- 35 | if ($route->action == "authcheck") { $route->action = "auth"; $route->subaction = "check"; } 36 | if ($route->action == "authallow") { $route->action = "auth"; $route->subaction = "allow"; } 37 | 38 | if ($route->action == "auth") { 39 | if ($route->subaction=="request") { 40 | // 1. Register request for authentication details, or provide if allowed 41 | $result = $device->request_auth($_SERVER['REMOTE_ADDR']); 42 | if (isset($result['success'])) { 43 | $result = $result['message']; 44 | } 45 | $route->format = "text"; 46 | } 47 | else if ($route->subaction=="check" && $session['read']) { 48 | // 2. User checks for device waiting for authentication 49 | $result = $device->get_auth_request(); 50 | 51 | if (isset($settings["device"]["enable_UDP_broadcast"]) && $settings["device"]["enable_UDP_broadcast"]) { 52 | $port = 5005; 53 | $broadcast_string = "emonpi.local"; 54 | $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); 55 | socket_set_option($sock, SOL_SOCKET, SO_BROADCAST, 1); 56 | socket_sendto($sock, $broadcast_string, strlen($broadcast_string), 0, '255.255.255.255', $port); 57 | } 58 | } 59 | else if ($route->subaction=="allow" && $session['write']) { 60 | // 3. User allows device to receive authentication details 61 | $result = $device->allow_auth_request(get("ip")); 62 | } 63 | } 64 | else if ($route->action == 'list') { 65 | if ($session['userid']>0 && $session['read']) $result = $device->get_list($session['userid']); 66 | } 67 | else if ($route->action == "create") { 68 | if ($session['userid']>0 && $session['write']) $result = $device->create($session['userid'],get("nodeid"),get("name"),get("description"),get("type"),get("options")); 69 | } 70 | // Used in conjunction with input name describe to auto create device 71 | else if ($route->action == "autocreate") { 72 | if ($session['userid']>0 && $session['write']) $result = $device->autocreate($session['userid'],get('nodeid'),get('type')); 73 | } 74 | else if ($route->action == "template" && $route->subaction != "prepare" && $route->subaction != "init") { 75 | if ($route->subaction == "listshort") { 76 | if ($session['userid']>0 && $session['write']) $result = $device->get_template_list_meta(); 77 | } 78 | else if ($route->subaction == "list") { 79 | if ($session['userid']>0 && $session['write']) $result = $device->get_template_list(); 80 | } 81 | else if ($route->subaction == "reload") { 82 | if ($session['userid']>0 && $session['write']) $result = $device->reload_template_list(); 83 | } 84 | else if ($route->subaction == "get") { 85 | if ($session['userid']>0 && $session['write']) $result = $device->get_template(get('type')); 86 | } 87 | } 88 | else { 89 | $deviceid = (int) get('id'); 90 | if ($device->exist($deviceid)) // if the feed exists 91 | { 92 | $deviceget = $device->get($deviceid); 93 | if (isset($session['write']) && $session['write'] && $session['userid']>0 && $deviceget['userid']==$session['userid']) { 94 | if ($route->action == "get") $result = $deviceget; 95 | else if ($route->action == 'set') $result = $device->set_fields($deviceid, get('fields')); 96 | else if ($route->action == 'init') $result = $device->init($deviceid, prop('template')); 97 | else if ($route->action == "delete") $result = $device->delete($deviceid); 98 | else if ($route->action == "setnewdevicekey") $result = $device->set_new_devicekey($deviceid); 99 | else if ($route->action == 'template') { 100 | if (isset($_GET['type'])) { 101 | $device->set_fields($deviceid, json_encode(array("type"=>$_GET['type']))); 102 | } 103 | if ($route->subaction == 'prepare') $result = $device->prepare_template($deviceid); 104 | else if ($route->subaction == 'init') $result = $device->init_template($deviceget, $_POST['template']); 105 | } 106 | } 107 | } 108 | else { 109 | $result = array('success'=>false, 'message'=>'Device does not exist'); 110 | } 111 | } 112 | } 113 | 114 | 115 | if ($route->action == "clean" && $session['write']) { 116 | $route->format = 'text'; 117 | $active = 0; if (isset($_GET['active'])) $active = (int) $_GET['active']; 118 | $dryrun = 0; if (isset($_GET['dryrun']) && $_GET['dryrun']==1) $dryrun = 1; 119 | return $device->clean($session['userid'],$active,$dryrun); 120 | } 121 | 122 | return array('content'=>$result); 123 | } 124 | -------------------------------------------------------------------------------- /Views/device_view.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 |
27 |
28 |

29 | 30 |
31 | 32 |
33 |
34 |


35 |

36 | 37 |

38 | 39 |
40 | 41 |

42 |
43 |
44 | 45 |

46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 | 55 | 56 | 57 | 168 | -------------------------------------------------------------------------------- /Views/device_dialog.css: -------------------------------------------------------------------------------- 1 | .modal-adjust { 2 | width: 88%; 3 | left: 6%; /* (100%-width)/2 */ 4 | margin-left: auto; 5 | margin-right: auto; 6 | overflow-y: hidden; 7 | } 8 | @media (min-width: 1280px) { 9 | .modal-adjust { 10 | width: 60%; 11 | left: 20%; /* (100%-width)/2 */ 12 | margin-left: auto; 13 | margin-right: auto; 14 | overflow-y: hidden; 15 | } 16 | } 17 | 18 | .modal-adjust .modal-body { 19 | max-height: none; 20 | overflow-y: hidden; 21 | } 22 | 23 | .modal-adjust .tooltip-inner { 24 | max-width: 250px; 25 | } 26 | 27 | .modal-adjust .divider { 28 | *width: 100%; 29 | height: 1px; 30 | margin: 9px 1px; 31 | *margin: -5px 0 5px; 32 | overflow: hidden; 33 | background-color: #e5e5e5; 34 | border-bottom: 1px solid #ffffff; 35 | } 36 | 37 | .modal-adjust .alert-comment { 38 | margin: 0px; 39 | color: #737373; 40 | background-color: #f9f9f9; 41 | border-color: #e6e6e6; 42 | } 43 | 44 | .modal-adjust .alert-comment h4 { 45 | color: #737373; 46 | } 47 | 48 | .modal-adjust .content { 49 | position: absolute; 50 | left: 15px; 51 | right: 0px; 52 | bottom: 0px; 53 | height: 100%; 54 | overflow-y: auto; 55 | } 56 | 57 | .modal-overlay { 58 | position: absolute; 59 | top: 0; bottom:0; left: 0; right:0; 60 | margin: auto; 61 | background: rgba(255, 255, 255, .8) center; 62 | } 63 | .modal-content { 64 | position: absolute; 65 | left: 15px; 66 | right: 0px; 67 | padding-right: 15px; 68 | margin-left: 15px; 69 | margin-top: -15px; 70 | height: 100%; 71 | max-height: none; 72 | overflow-y: auto; 73 | } 74 | 75 | .modal-content .btn-sidebar { 76 | margin-bottom: 5px; 77 | } 78 | .modal-sidebar { 79 | position: absolute; 80 | margin-top: -15px; 81 | margin-left: -15px; 82 | max-height: none; 83 | height: 100%; 84 | width: 250px; 85 | overflow-x: hidden; 86 | overflow-y: auto; 87 | background-color: #f2f2f2; 88 | z-index: 1000; 89 | } 90 | .modal-sidebar .accordion { 91 | margin-bottom: 0px; 92 | } 93 | .modal-sidebar .accordion-group { 94 | margin-bottom: 0px; 95 | border: 0px; 96 | -webkit-border-radius: 0px; 97 | -moz-border-radius: 0px; 98 | border-radius: 0px; 99 | } 100 | .modal-sidebar .accordion-inner { 101 | padding: 0px; 102 | border-top: 0px; 103 | } 104 | .modal-sidebar .category-heading { 105 | background-color: #cccccc; 106 | border-bottom: 1px solid #d9d9d9; 107 | cursor: pointer; 108 | } 109 | .modal-sidebar .category-heading:hover { 110 | background-color: #d9d9d9; 111 | } 112 | .modal-sidebar .category-heading span { 113 | font-weight: bold; 114 | padding: 6px 6px 6px 10px; 115 | } 116 | .modal-sidebar .group-heading { 117 | background-color: #e6e6e6; 118 | border-bottom: 1px solid #ededed; 119 | cursor: pointer; 120 | } 121 | .modal-sidebar .group-heading:hover { 122 | background-color: #ededed; 123 | } 124 | .modal-sidebar .group-heading span { 125 | font-weight: bold; 126 | padding: 6px 6px 6px 16px; 127 | } 128 | .modal-sidebar .group-device { 129 | padding: 8px 8px 8px 22px; 130 | background-color: #f5f5f5; 131 | border-bottom: 1px solid #fcfcfc; 132 | cursor: pointer; 133 | } 134 | .modal-sidebar .group-device:hover { 135 | background-color: #44b3e2; 136 | color: #ffffff; 137 | } 138 | 139 | .device-selected { 140 | background-color: #209ed3 !important; 141 | color: #ffffff; 142 | } 143 | 144 | .device-input { 145 | width: 100%; 146 | } 147 | .device-input th, 148 | .device-input td { 149 | white-space: nowrap; 150 | text-align: left; 151 | } 152 | .device-input th:nth-of-type(3) { 153 | color: #888; 154 | font-weight: normal; 155 | } 156 | .device-input td:nth-of-type(1) { width:108px; } 157 | .device-input td:nth-of-type(2) { width:168px; } 158 | 159 | .table-feeds td:nth-of-type(1) { width:14px; text-align: center; } 160 | .table-feeds td:nth-of-type(2) { width:5%; } 161 | .table-feeds td:nth-of-type(3) { width:15%; } 162 | .table-feeds td:nth-of-type(4) { width:25%; } 163 | 164 | .table-inputs td:nth-of-type(1) { width:14px; text-align: center; } 165 | .table-inputs td:nth-of-type(2) { width:5%; } 166 | .table-inputs td:nth-of-type(3) { width:5%; } 167 | .table-inputs td:nth-of-type(4) { width:10%; } 168 | .table-inputs td:nth-of-type(5) { width:25%; } 169 | 170 | 171 | #device-config-modal.modal { 172 | margin: 0; 173 | border-radius: 0; 174 | border: none; 175 | width: 100%; 176 | left: 0; 177 | top: 3%; 178 | } 179 | #device-config-body { 180 | padding: 0 181 | } 182 | #device-config-modal.modal .accordion-toggle { 183 | white-space: nowrap; 184 | overflow: hidden; 185 | } 186 | #device-content { 187 | left: 0.2rem; 188 | margin-left: 0; 189 | transition: width 0.5s ease-in-out; 190 | } 191 | #device-sidebar-close { 192 | cursor: pointer; 193 | position: absolute; 194 | right: 0; 195 | top: .5em; 196 | line-height: 1.3; 197 | background: #f2f2f2; 198 | padding: .3em .6em; 199 | } 200 | #device-sidebar { 201 | transition: width 0.3s ease-in-out; 202 | width: 0; 203 | } 204 | #device-sidebar.show { 205 | width: 100%; 206 | } 207 | .key { 208 | font-size: 12px !important; 209 | font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; 210 | } 211 | #device-config-modal.modal .modal-content { 212 | margin-top: 0 213 | } 214 | #device-config-modal.modal .modal-sidebar { 215 | margin: 0 216 | } 217 | 218 | /* add horizontal padding to devices larger than iphone5 (320px)*/ 219 | @media (min-width: 340px) { 220 | #device-config-modal.modal { 221 | margin: 0 auto 222 | } 223 | #device-config-modal.modal .modal-sidebar { 224 | margin: 0 -1rem 225 | } 226 | #device-content { 227 | left: 15px; 228 | } 229 | #device-config-body { 230 | padding: 0 1rem 231 | } 232 | } 233 | 234 | @media (min-width: 680px) { 235 | #device-sidebar.show { 236 | width: 250px; 237 | transition: none; 238 | } 239 | #device-content { 240 | left: 270px; 241 | } 242 | #device-config-modal.modal { 243 | margin: 0; 244 | border-radius: 6px; 245 | border: 1px solid rgba(0,0,0,0.3); 246 | width: 94%; 247 | left: 3%; 248 | } 249 | #device-config-modal.modal .modal-sidebar { 250 | margin: 0 -1rem 251 | } 252 | 253 | } 254 | 255 | /* hide/show items on very small devices */ 256 | .visible-xs { 257 | display: none 258 | } 259 | @media (max-width: 375px) { 260 | .hidden-xs { 261 | display: none 262 | } 263 | .visible-xs { 264 | display: inline-block 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Views/device_dialog.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 68 | 69 | 115 | 116 | 136 | 137 | 143 | -------------------------------------------------------------------------------- /data/CircuitSetup/circuitsetup-split-phase_solar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solar Kit", 3 | "category": "CircuitSetup", 4 | "group": "Energy Meters", 5 | "description": "CircuitSetup Split Single Phase Energy Meter with Solar", 6 | "inputs": [ 7 | { 8 | "name": "V1", 9 | "description": "Voltage 1", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 1" } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "V2", 19 | "description": "Voltage 2", 20 | "processList": [ 21 | { 22 | "process": "log_to_feed", 23 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 2" } 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "totV", 29 | "description": "Total Voltage", 30 | "processList": [ 31 | { 32 | "process": "log_to_feed", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Voltage" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "SolarV", 39 | "description": "Solar Voltage", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Solar Voltage" } 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "CT1", 49 | "description": "Current 1", 50 | "processList": [ 51 | { 52 | "process": "log_to_feed", 53 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 1" } 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "CT2", 59 | "description": "Current 2", 60 | "processList": [ 61 | { 62 | "process": "log_to_feed", 63 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Current 2" } 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "totI", 69 | "description": "Total Current", 70 | "processList": [ 71 | { 72 | "process": "log_to_feed", 73 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Current" } 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "SCT1", 79 | "description": "Solar Current 1", 80 | "processList": [ 81 | { 82 | "process": "log_to_feed", 83 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Solar Current 1" } 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "SCT2", 89 | "description": "Solar Current 2", 90 | "processList": [ 91 | { 92 | "process": "log_to_feed", 93 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Solar Current 2" } 94 | } 95 | ] 96 | }, 97 | { 98 | "name": "totSolarI", 99 | "description": "Total Solar Current", 100 | "processList": [ 101 | { 102 | "process": "log_to_feed", 103 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Total Solar Current" } 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "PF", 109 | "description": "Power Factor", 110 | "processList": [ 111 | { 112 | "process": "log_to_feed", 113 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Power Factor" } 114 | } 115 | ] 116 | }, 117 | { 118 | "name": "temp", 119 | "description": "Temp", 120 | "processList": [ 121 | { 122 | "process": "log_to_feed", 123 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Temp" } 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "freq", 129 | "description": "Frequency", 130 | "processList": [ 131 | { 132 | "process": "log_to_feed", 133 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Frequency" } 134 | } 135 | ] 136 | }, 137 | { 138 | "name": "W", 139 | "description": "Power Usage", 140 | "processList": [ 141 | { 142 | "process": "allowpositive", 143 | "arguments": {"type": "ProcessArg::NONE"} 144 | }, 145 | { 146 | "process": "log_to_feed", 147 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import" } 148 | }, 149 | { 150 | "process": "power_to_kwh", 151 | "arguments": {"type": "ProcessArg::FEEDID", "value": "import_kwh" } 152 | } 153 | ] 154 | }, 155 | { 156 | "name": "SolarW", 157 | "description": "Solar Power", 158 | "processList": [ 159 | { 160 | "process": "log_to_feed", 161 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 162 | }, 163 | { 164 | "process": "power_to_kwh", 165 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 166 | }, 167 | { 168 | "process": "add_input", 169 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W" } 170 | }, 171 | { 172 | "process": "log_to_feed", 173 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 174 | }, 175 | { 176 | "process": "power_to_kwh", 177 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 178 | } 179 | ] 180 | } 181 | ], 182 | 183 | "feeds": [ 184 | { 185 | "name": "Voltage 1", 186 | "type": "DataType::REALTIME", 187 | "engine": "Engine::PHPFINA", 188 | "interval": "10", 189 | "unit": "V" 190 | }, 191 | { 192 | "name": "Voltage 2", 193 | "type": "DataType::REALTIME", 194 | "engine": "Engine::PHPFINA", 195 | "interval": "10", 196 | "unit": "V" 197 | }, 198 | { 199 | "name": "Total Voltage", 200 | "type": "DataType::REALTIME", 201 | "engine": "Engine::PHPFINA", 202 | "interval": "10", 203 | "unit": "V" 204 | }, 205 | { 206 | "name": "Solar Voltage", 207 | "type": "DataType::REALTIME", 208 | "engine": "Engine::PHPFINA", 209 | "interval": "10", 210 | "unit": "V" 211 | }, 212 | { 213 | "name": "Current 1", 214 | "type": "DataType::REALTIME", 215 | "engine": "Engine::PHPFINA", 216 | "interval": "10", 217 | "unit": "A" 218 | }, 219 | { 220 | "name": "Current 2", 221 | "type": "DataType::REALTIME", 222 | "engine": "Engine::PHPFINA", 223 | "interval": "10", 224 | "unit": "A" 225 | }, 226 | { 227 | "name": "Total Current", 228 | "type": "DataType::REALTIME", 229 | "engine": "Engine::PHPFINA", 230 | "interval": "10", 231 | "unit": "A" 232 | }, 233 | { 234 | "name": "Solar Current 1", 235 | "type": "DataType::REALTIME", 236 | "engine": "Engine::PHPFINA", 237 | "interval": "10", 238 | "unit": "A" 239 | }, 240 | { 241 | "name": "Solar Current 2", 242 | "type": "DataType::REALTIME", 243 | "engine": "Engine::PHPFINA", 244 | "interval": "10", 245 | "unit": "A" 246 | }, 247 | { 248 | "name": "Total Solar Current", 249 | "type": "DataType::REALTIME", 250 | "engine": "Engine::PHPFINA", 251 | "interval": "10", 252 | "unit": "A" 253 | }, 254 | { 255 | "name": "use", 256 | "type": "DataType::REALTIME", 257 | "engine": "Engine::PHPFINA", 258 | "interval": "10", 259 | "unit": "W" 260 | }, 261 | { 262 | "name": "use_kwh", 263 | "type": "DataType::REALTIME", 264 | "engine": "Engine::PHPFINA", 265 | "interval": "10", 266 | "unit": "kWh" 267 | }, 268 | { 269 | "name": "solar", 270 | "type": "DataType::REALTIME", 271 | "engine": "Engine::PHPFINA", 272 | "interval": "10", 273 | "unit": "W" 274 | }, 275 | { 276 | "name": "solar_kwh", 277 | "type": "DataType::REALTIME", 278 | "engine": "Engine::PHPFINA", 279 | "interval": "10", 280 | "unit": "kWh" 281 | }, 282 | { 283 | "name": "import", 284 | "type": "DataType::REALTIME", 285 | "engine": "Engine::PHPFINA", 286 | "interval": "10", 287 | "unit": "W" 288 | }, 289 | { 290 | "name": "import_kwh", 291 | "type": "DataType::REALTIME", 292 | "engine": "Engine::PHPFINA", 293 | "interval": "10", 294 | "unit": "kWh" 295 | }, 296 | { 297 | "name": "Power Factor", 298 | "type": "DataType::REALTIME", 299 | "engine": "Engine::PHPFINA", 300 | "interval": "10", 301 | "unit": "" 302 | }, 303 | { 304 | "name": "Temp", 305 | "type": "DataType::REALTIME", 306 | "engine": "Engine::PHPFINA", 307 | "interval": "10", 308 | "unit": "°C" 309 | }, 310 | { 311 | "name": "Frequency", 312 | "type": "DataType::REALTIME", 313 | "engine": "Engine::PHPFINA", 314 | "interval": "10", 315 | "unit": "" 316 | } 317 | ] 318 | } 319 | -------------------------------------------------------------------------------- /data/CircuitSetup/circuitsetup-6_channel_solar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6 Channel Meter with Solar", 3 | "category": "CircuitSetup", 4 | "group": "Energy Meters", 5 | "description": "CircuitSetup 6 Channel with Solar", 6 | "inputs": [ 7 | { 8 | "name": "V1", 9 | "description": "Voltage 1", 10 | "processList": [ 11 | { 12 | "process": "log_to_feed", 13 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 1" } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "V2", 19 | "description": "Voltage 2", 20 | "processList": [ 21 | { 22 | "process": "log_to_feed", 23 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Voltage 2" } 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "CT1", 29 | "description": "House Current 1", 30 | "processList": [ 31 | { 32 | "process": "log_to_feed", 33 | "arguments": {"type": "ProcessArg::FEEDID", "value": "House Current 1" } 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "CT2", 39 | "description": "House Current 2", 40 | "processList": [ 41 | { 42 | "process": "log_to_feed", 43 | "arguments": {"type": "ProcessArg::FEEDID", "value": "House Current 2" } 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "CT3", 49 | "description": "Charging Current 3", 50 | "processList": [ 51 | { 52 | "process": "log_to_feed", 53 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Charging Current 3" } 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "CT4", 59 | "description": "Charging Current 4", 60 | "processList": [ 61 | { 62 | "process": "log_to_feed", 63 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Charging Current 4" } 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "CT5", 69 | "description": "Solar Current 5", 70 | "processList": [ 71 | { 72 | "process": "log_to_feed", 73 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Solar Current 5" } 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "CT6", 79 | "description": "Solar Current 6", 80 | "processList": [ 81 | { 82 | "process": "log_to_feed", 83 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Solar Current 6" } 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "PF1", 89 | "description": "House Power Factor 1", 90 | "processList": [ 91 | { 92 | "process": "log_to_feed", 93 | "arguments": {"type": "ProcessArg::FEEDID", "value": "House PF 1" } 94 | } 95 | ] 96 | }, 97 | { 98 | "name": "PF2", 99 | "description": "House Power Factor 2", 100 | "processList": [ 101 | { 102 | "process": "log_to_feed", 103 | "arguments": {"type": "ProcessArg::FEEDID", "value": "House PF 2" } 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "temp", 109 | "description": "Temp", 110 | "processList": [ 111 | { 112 | "process": "log_to_feed", 113 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Temp" } 114 | } 115 | ] 116 | }, 117 | { 118 | "name": "freq", 119 | "description": "Frequency", 120 | "processList": [ 121 | { 122 | "process": "log_to_feed", 123 | "arguments": {"type": "ProcessArg::FEEDID", "value": "Frequency" } 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "W1", 129 | "description": "Total House Power", 130 | "processList": [ 131 | { 132 | "process": "add_input", 133 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W2" } 134 | }, 135 | { 136 | "process": "log_to_feed", 137 | "arguments": {"type": "ProcessArg::FEEDID", "value": "house_import" } 138 | }, 139 | { 140 | "process": "power_to_kwh", 141 | "arguments": {"type": "ProcessArg::FEEDID", "value": "house_import_kwh" } 142 | } 143 | ] 144 | }, 145 | { 146 | "name": "W3", 147 | "description": "Total Charging", 148 | "processList": [ 149 | { 150 | "process": "allowpositive", 151 | "arguments": {"type": "ProcessArg::NONE"} 152 | }, 153 | { 154 | "process": "add_input", 155 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W4" } 156 | }, 157 | { 158 | "process": "log_to_feed", 159 | "arguments": {"type": "ProcessArg::FEEDID", "value": "charging_import" } 160 | }, 161 | { 162 | "process": "power_to_kwh", 163 | "arguments": {"type": "ProcessArg::FEEDID", "value": "charging_import_kwh" } 164 | } 165 | ] 166 | }, 167 | { 168 | "name": "W5", 169 | "description": "Total Solar Power", 170 | "processList": [ 171 | { 172 | "process": "add_input", 173 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W6" } 174 | }, 175 | { 176 | "process": "log_to_feed", 177 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar" } 178 | }, 179 | { 180 | "process": "power_to_kwh", 181 | "arguments": {"type": "ProcessArg::FEEDID", "value": "solar_kwh" } 182 | }, 183 | { 184 | "process": "add_input", 185 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W1" } 186 | }, 187 | { 188 | "process": "add_input", 189 | "arguments": {"type": "ProcessArg::INPUTID", "value": "W2" } 190 | }, 191 | { 192 | "process": "log_to_feed", 193 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use" } 194 | }, 195 | { 196 | "process": "power_to_kwh", 197 | "arguments": {"type": "ProcessArg::FEEDID", "value": "use_kwh" } 198 | } 199 | ] 200 | } 201 | ], 202 | 203 | "feeds": [ 204 | { 205 | "name": "Voltage 1", 206 | "type": "DataType::REALTIME", 207 | "engine": "Engine::PHPFINA", 208 | "interval": "10", 209 | "unit": "V" 210 | }, 211 | { 212 | "name": "Voltage 2", 213 | "type": "DataType::REALTIME", 214 | "engine": "Engine::PHPFINA", 215 | "interval": "10", 216 | "unit": "V" 217 | }, 218 | { 219 | "name": "House Current 1", 220 | "type": "DataType::REALTIME", 221 | "engine": "Engine::PHPFINA", 222 | "interval": "10", 223 | "unit": "A" 224 | }, 225 | { 226 | "name": "House Current 2", 227 | "type": "DataType::REALTIME", 228 | "engine": "Engine::PHPFINA", 229 | "interval": "10", 230 | "unit": "A" 231 | }, 232 | { 233 | "name": "Charging Current 3", 234 | "type": "DataType::REALTIME", 235 | "engine": "Engine::PHPFINA", 236 | "interval": "10", 237 | "unit": "A" 238 | }, 239 | { 240 | "name": "Charging Current 4", 241 | "type": "DataType::REALTIME", 242 | "engine": "Engine::PHPFINA", 243 | "interval": "10", 244 | "unit": "A" 245 | }, 246 | { 247 | "name": "Solar Current 5", 248 | "type": "DataType::REALTIME", 249 | "engine": "Engine::PHPFINA", 250 | "interval": "10", 251 | "unit": "A" 252 | }, 253 | { 254 | "name": "Solar Current 6", 255 | "type": "DataType::REALTIME", 256 | "engine": "Engine::PHPFINA", 257 | "interval": "10", 258 | "unit": "A" 259 | }, 260 | { 261 | "name": "charging_import", 262 | "type": "DataType::REALTIME", 263 | "engine": "Engine::PHPFINA", 264 | "interval": "10", 265 | "unit": "W" 266 | }, 267 | { 268 | "name": "charging_import_kwh", 269 | "type": "DataType::REALTIME", 270 | "engine": "Engine::PHPFINA", 271 | "interval": "10", 272 | "unit": "kWh" 273 | }, 274 | { 275 | "name": "use", 276 | "type": "DataType::REALTIME", 277 | "engine": "Engine::PHPFINA", 278 | "interval": "10", 279 | "unit": "W" 280 | }, 281 | { 282 | "name": "use_kwh", 283 | "type": "DataType::REALTIME", 284 | "engine": "Engine::PHPFINA", 285 | "interval": "10", 286 | "unit": "kWh" 287 | }, 288 | { 289 | "name": "solar", 290 | "type": "DataType::REALTIME", 291 | "engine": "Engine::PHPFINA", 292 | "interval": "10", 293 | "unit": "W" 294 | }, 295 | { 296 | "name": "solar_kwh", 297 | "type": "DataType::REALTIME", 298 | "engine": "Engine::PHPFINA", 299 | "interval": "10", 300 | "unit": "kWh" 301 | }, 302 | { 303 | "name": "house_import", 304 | "type": "DataType::REALTIME", 305 | "engine": "Engine::PHPFINA", 306 | "interval": "10", 307 | "unit": "W" 308 | }, 309 | { 310 | "name": "house_import_kwh", 311 | "type": "DataType::REALTIME", 312 | "engine": "Engine::PHPFINA", 313 | "interval": "10", 314 | "unit": "kWh" 315 | }, 316 | { 317 | "name": "House PF 1", 318 | "type": "DataType::REALTIME", 319 | "engine": "Engine::PHPFINA", 320 | "interval": "10", 321 | "unit": "" 322 | }, 323 | { 324 | "name": "House PF 2", 325 | "type": "DataType::REALTIME", 326 | "engine": "Engine::PHPFINA", 327 | "interval": "10", 328 | "unit": "" 329 | }, 330 | { 331 | "name": "Temp", 332 | "type": "DataType::REALTIME", 333 | "engine": "Engine::PHPFINA", 334 | "interval": "10", 335 | "unit": "°C" 336 | }, 337 | { 338 | "name": "Frequency", 339 | "type": "DataType::REALTIME", 340 | "engine": "Engine::PHPFINA", 341 | "interval": "10", 342 | "unit": "" 343 | } 344 | ] 345 | } 346 | -------------------------------------------------------------------------------- /data/amig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AMIG", 3 | "category": "Datalogger", 4 | "group": "ArchiMetrics", 5 | "description": "INPUTS & FEEDS for AMIG Logger from archimetrics.co.uk", 6 | "inputs": [{ 7 | "name": "n1T", 8 | "description": "N1 Temperature", 9 | "processList": [{ 10 | "process": "log_to_feed", 11 | "arguments": { 12 | "type": "ProcessArg::FEEDID", 13 | "value": "n1T" 14 | } 15 | }] 16 | }, 17 | { 18 | "name": "n1R", 19 | "description": "N1 RH", 20 | "processList": [{ 21 | "process": "log_to_feed", 22 | "arguments": { 23 | "type": "ProcessArg::FEEDID", 24 | "value": "n1R" 25 | } 26 | }] 27 | }, 28 | { 29 | "name": "n1D", 30 | "description": "N1 DewPoint", 31 | "processList": [{ 32 | "process": "log_to_feed", 33 | "arguments": { 34 | "type": "ProcessArg::FEEDID", 35 | "value": "n1D" 36 | } 37 | }] 38 | }, 39 | { 40 | "name": "n2T", 41 | "description": "N2 Temperature", 42 | "processList": [{ 43 | "process": "log_to_feed", 44 | "arguments": { 45 | "type": "ProcessArg::FEEDID", 46 | "value": "n2T" 47 | } 48 | }] 49 | }, 50 | { 51 | "name": "n2R", 52 | "description": "N2 RH", 53 | "processList": [{ 54 | "process": "log_to_feed", 55 | "arguments": { 56 | "type": "ProcessArg::FEEDID", 57 | "value": "n2R" 58 | } 59 | }] 60 | }, 61 | { 62 | "name": "n2D", 63 | "description": "N2 DewPoint", 64 | "processList": [{ 65 | "process": "log_to_feed", 66 | "arguments": { 67 | "type": "ProcessArg::FEEDID", 68 | "value": "n2D" 69 | } 70 | }] 71 | }, 72 | { 73 | "name": "n3T", 74 | "description": "N3 Temperature", 75 | "processList": [{ 76 | "process": "log_to_feed", 77 | "arguments": { 78 | "type": "ProcessArg::FEEDID", 79 | "value": "n3T" 80 | } 81 | }] 82 | }, 83 | { 84 | "name": "n3R", 85 | "description": "N3 RH", 86 | "processList": [{ 87 | "process": "log_to_feed", 88 | "arguments": { 89 | "type": "ProcessArg::FEEDID", 90 | "value": "n3R" 91 | } 92 | }] 93 | }, 94 | { 95 | "name": "n3D", 96 | "description": "N3 DewPoint", 97 | "processList": [{ 98 | "process": "log_to_feed", 99 | "arguments": { 100 | "type": "ProcessArg::FEEDID", 101 | "value": "n3D" 102 | } 103 | }] 104 | }, 105 | { 106 | "name": "n4T", 107 | "description": "N4 Temperature", 108 | "processList": [{ 109 | "process": "log_to_feed", 110 | "arguments": { 111 | "type": "ProcessArg::FEEDID", 112 | "value": "n4T" 113 | } 114 | }] 115 | }, 116 | { 117 | "name": "n4R", 118 | "description": "N4 RH", 119 | "processList": [{ 120 | "process": "log_to_feed", 121 | "arguments": { 122 | "type": "ProcessArg::FEEDID", 123 | "value": "n4R" 124 | } 125 | }] 126 | }, 127 | { 128 | "name": "n4D", 129 | "description": "N4 DewPoint", 130 | "processList": [{ 131 | "process": "log_to_feed", 132 | "arguments": { 133 | "type": "ProcessArg::FEEDID", 134 | "value": "n4D" 135 | } 136 | }] 137 | }, 138 | { 139 | "name": "iTS", 140 | "description": "Internal Temperature Surface", 141 | "processList": [{ 142 | "process": "log_to_feed", 143 | "arguments": { 144 | "type": "ProcessArg::FEEDID", 145 | "value": "iTS" 146 | } 147 | }] 148 | }, 149 | { 150 | "name": "eTS", 151 | "description": "External Temperature Surface", 152 | "processList": [{ 153 | "process": "log_to_feed", 154 | "arguments": { 155 | "type": "ProcessArg::FEEDID", 156 | "value": "eTS" 157 | } 158 | }] 159 | }, 160 | { 161 | "name": "eSR", 162 | "description": "External Solar Radiation", 163 | "processList": [{ 164 | "process": "log_to_feed", 165 | "arguments": { 166 | "type": "ProcessArg::FEEDID", 167 | "value": "eSR" 168 | } 169 | }] 170 | }, 171 | { 172 | "name": "iTA", 173 | "description": "Internal Temperature Air", 174 | "processList": [{ 175 | "process": "log_to_feed", 176 | "arguments": { 177 | "type": "ProcessArg::FEEDID", 178 | "value": "iTA" 179 | } 180 | }] 181 | }, 182 | { 183 | "name": "iRH", 184 | "description": "Internal RH", 185 | "processList": [{ 186 | "process": "log_to_feed", 187 | "arguments": { 188 | "type": "ProcessArg::FEEDID", 189 | "value": "iRH" 190 | } 191 | }] 192 | }, 193 | { 194 | "name": "iDP", 195 | "description": "Internal DewPoint", 196 | "processList": [{ 197 | "process": "log_to_feed", 198 | "arguments": { 199 | "type": "ProcessArg::FEEDID", 200 | "value": "iDP" 201 | } 202 | }] 203 | }, 204 | { 205 | "name": "eTA", 206 | "description": "External Temperature Air", 207 | "processList": [{ 208 | "process": "log_to_feed", 209 | "arguments": { 210 | "type": "ProcessArg::FEEDID", 211 | "value": "eTA" 212 | } 213 | }] 214 | }, 215 | { 216 | "name": "eRH", 217 | "description": "External RH", 218 | "processList": [{ 219 | "process": "log_to_feed", 220 | "arguments": { 221 | "type": "ProcessArg::FEEDID", 222 | "value": "eRH" 223 | } 224 | }] 225 | }, 226 | { 227 | "name": "eDP", 228 | "description": "External DewPoint", 229 | "processList": [{ 230 | "process": "log_to_feed", 231 | "arguments": { 232 | "type": "ProcessArg::FEEDID", 233 | "value": "eDP" 234 | } 235 | }] 236 | }, 237 | { 238 | "name": "hfV", 239 | "description": "HeatFlux Voltage", 240 | "processList": [{ 241 | "process": "log_to_feed", 242 | "arguments": { 243 | "type": "ProcessArg::FEEDID", 244 | "value": "hfV" 245 | } 246 | }] 247 | }, 248 | { 249 | "name": "hfW", 250 | "description": "HeatFlux Watts", 251 | "processList": [{ 252 | "process": "log_to_feed", 253 | "arguments": { 254 | "type": "ProcessArg::FEEDID", 255 | "value": "hfW" 256 | } 257 | }] 258 | }, 259 | { 260 | "name": "CO2", 261 | "description": "Carbon Dioxide", 262 | "processList": [{ 263 | "process": "log_to_feed", 264 | "arguments": { 265 | "type": "ProcessArg::FEEDID", 266 | "value": "CO2" 267 | } 268 | }] 269 | }, 270 | { 271 | "name": "SYS", 272 | "description": "System Temperature", 273 | "processList": [{ 274 | "process": "log_to_feed", 275 | "arguments": { 276 | "type": "ProcessArg::FEEDID", 277 | "value": "SYS" 278 | } 279 | }] 280 | }], 281 | "feeds": [{ 282 | "name": "n1T", 283 | "type": "DataType::REALTIME", 284 | "engine": "Engine::PHPFINA", 285 | "interval": "10" 286 | }, 287 | { 288 | "name": "n1R", 289 | "type": "DataType::REALTIME", 290 | "engine": "Engine::PHPFINA", 291 | "interval": "10" 292 | }, 293 | { 294 | "name": "n1D", 295 | "type": "DataType::REALTIME", 296 | "engine": "Engine::PHPFINA", 297 | "interval": "10" 298 | }, 299 | { 300 | "name": "n2T", 301 | "type": "DataType::REALTIME", 302 | "engine": "Engine::PHPFINA", 303 | "interval": "10" 304 | }, 305 | { 306 | "name": "n2R", 307 | "type": "DataType::REALTIME", 308 | "engine": "Engine::PHPFINA", 309 | "interval": "10" 310 | }, 311 | { 312 | "name": "n2D", 313 | "type": "DataType::REALTIME", 314 | "engine": "Engine::PHPFINA", 315 | "interval": "10" 316 | }, 317 | { 318 | "name": "n3T", 319 | "type": "DataType::REALTIME", 320 | "engine": "Engine::PHPFINA", 321 | "interval": "10" 322 | }, 323 | { 324 | "name": "n3R", 325 | "type": "DataType::REALTIME", 326 | "engine": "Engine::PHPFINA", 327 | "interval": "10" 328 | }, 329 | { 330 | "name": "n3D", 331 | "type": "DataType::REALTIME", 332 | "engine": "Engine::PHPFINA", 333 | "interval": "10" 334 | }, 335 | { 336 | "name": "n4T", 337 | "type": "DataType::REALTIME", 338 | "engine": "Engine::PHPFINA", 339 | "interval": "10" 340 | }, 341 | { 342 | "name": "n4R", 343 | "type": "DataType::REALTIME", 344 | "engine": "Engine::PHPFINA", 345 | "interval": "10" 346 | }, 347 | { 348 | "name": "n4D", 349 | "type": "DataType::REALTIME", 350 | "engine": "Engine::PHPFINA", 351 | "interval": "10" 352 | }, 353 | { 354 | "name": "iTS", 355 | "type": "DataType::REALTIME", 356 | "engine": "Engine::PHPFINA", 357 | "interval": "10" 358 | }, 359 | { 360 | "name": "eTS", 361 | "type": "DataType::REALTIME", 362 | "engine": "Engine::PHPFINA", 363 | "interval": "10" 364 | }, 365 | { 366 | "name": "eSR", 367 | "type": "DataType::REALTIME", 368 | "engine": "Engine::PHPFINA", 369 | "interval": "10" 370 | }, 371 | { 372 | "name": "iTA", 373 | "type": "DataType::REALTIME", 374 | "engine": "Engine::PHPFINA", 375 | "interval": "10" 376 | }, 377 | { 378 | "name": "iRH", 379 | "type": "DataType::REALTIME", 380 | "engine": "Engine::PHPFINA", 381 | "interval": "10" 382 | }, 383 | { 384 | "name": "iDP", 385 | "type": "DataType::REALTIME", 386 | "engine": "Engine::PHPFINA", 387 | "interval": "10" 388 | }, 389 | { 390 | "name": "eTA", 391 | "type": "DataType::REALTIME", 392 | "engine": "Engine::PHPFINA", 393 | "interval": "10" 394 | }, 395 | { 396 | "name": "eRH", 397 | "type": "DataType::REALTIME", 398 | "engine": "Engine::PHPFINA", 399 | "interval": "10" 400 | }, 401 | { 402 | "name": "eDP", 403 | "type": "DataType::REALTIME", 404 | "engine": "Engine::PHPFINA", 405 | "interval": "10" 406 | }, 407 | { 408 | "name": "hfV", 409 | "type": "DataType::REALTIME", 410 | "engine": "Engine::PHPFINA", 411 | "interval": "10" 412 | }, 413 | { 414 | "name": "hfW", 415 | "type": "DataType::REALTIME", 416 | "engine": "Engine::PHPFINA", 417 | "interval": "10" 418 | }, 419 | { 420 | "name": "CO2", 421 | "type": "DataType::REALTIME", 422 | "engine": "Engine::PHPFINA", 423 | "interval": "10" 424 | }, 425 | { 426 | "name": "SYS", 427 | "type": "DataType::REALTIME", 428 | "engine": "Engine::PHPFINA", 429 | "interval": "10" 430 | }] 431 | } 432 | -------------------------------------------------------------------------------- /device_template.php: -------------------------------------------------------------------------------- 1 | mysqli = &$parent->mysqli; 27 | $this->redis = &$parent->redis; 28 | $this->log = new EmonLogger(__FILE__); 29 | 30 | global $user,$settings; 31 | 32 | require_once "Modules/feed/feed_model.php"; 33 | $this->feed = new Feed($this->mysqli, $this->redis, $settings['feed']); 34 | 35 | require_once "Modules/input/input_model.php"; 36 | $this->input = new Input($this->mysqli, $this->redis, $this->feed); 37 | 38 | require_once "Modules/process/process_model.php"; 39 | $this->process = new Process($this->mysqli, $this->input, $this->feed,"UTC"); 40 | } 41 | 42 | public function get_template_list() { 43 | return $this->load_template_list(); 44 | } 45 | 46 | protected function load_template_list() { 47 | $list = array(); 48 | 49 | $iti = new RecursiveDirectoryIterator("Modules/device/data"); 50 | foreach(new RecursiveIteratorIterator($iti) as $file){ 51 | if(strpos($file ,".json") !== false){ 52 | $content = json_decode(file_get_contents($file)); 53 | if (json_last_error() != 0) { 54 | return array('success'=>false, 'message'=>"Error reading file $file: ".json_last_error_msg()); 55 | } 56 | $list[basename($file, ".json")] = $content; 57 | } 58 | } 59 | return $list; 60 | } 61 | 62 | public function get_template($type) { 63 | $type = preg_replace('/[^\p{L}_\p{N}\s\-:]/u','', $type); 64 | $result = $this->load_template_list(); 65 | if (isset($result['success']) && $result['success'] == false) { 66 | return $result; 67 | } 68 | if (!isset($result[$type])) { 69 | return array('success'=>false, 'message'=>'Device template "'.$type.'" not found'); 70 | } 71 | return $result[$type]; 72 | } 73 | 74 | public function prepare_template($device) { 75 | $userid = intval($device['userid']); 76 | 77 | $result = $this->get_template($device['type']); 78 | if (!is_object($result)) { 79 | return $result; 80 | } 81 | $prefix = $this->parse_prefix($device['nodeid'], $device['name'], $result); 82 | 83 | if (isset($result->feeds)) { 84 | $feeds = $result->feeds; 85 | $this->prepare_feeds($userid, $device['nodeid'], $prefix, $feeds); 86 | } 87 | else { 88 | $feeds = array(); 89 | } 90 | 91 | if (isset($result->inputs)) { 92 | $inputs = $result->inputs; 93 | $this->prepare_inputs($userid, $device['nodeid'], $prefix, $inputs); 94 | } 95 | else { 96 | $inputs = array(); 97 | } 98 | 99 | if (!empty($feeds)) { 100 | $this->prepare_feed_processes($userid, $prefix, $feeds, $inputs); 101 | } 102 | if (!empty($inputs)) { 103 | $this->prepare_input_processes($userid, $prefix, $feeds, $inputs); 104 | } 105 | 106 | return array('success'=>true, 'feeds'=>$feeds, 'inputs'=>$inputs); 107 | } 108 | 109 | public function init_template($device, $template) { 110 | $userid = intval($device['userid']); 111 | 112 | if (empty($template)) { 113 | $result = $this->prepare_template($device); 114 | if (isset($result['success']) && $result['success'] == false) { 115 | return $result; 116 | } 117 | $template = $result; 118 | } 119 | if (!is_object($template)) $template = (object) $template; 120 | 121 | if (isset($template->feeds)) { 122 | $feeds = $template->feeds; 123 | $this->create_feeds($userid, $feeds); 124 | } 125 | else { 126 | $feeds = array(); 127 | } 128 | 129 | if (isset($template->inputs)) { 130 | $inputs = $template->inputs; 131 | $this->create_inputs($userid, $inputs); 132 | } 133 | else { 134 | $inputs = array(); 135 | } 136 | 137 | if (!empty($feeds)) { 138 | $this->create_feed_processes($userid, $feeds, $inputs); 139 | } 140 | if (!empty($inputs)) { 141 | $this->create_input_processes($userid, $feeds, $inputs); 142 | } 143 | 144 | return array('success'=>true, 'message'=>'Device initialized'); 145 | } 146 | 147 | protected function prepare_feeds($userid, $nodeid, $prefix, &$feeds) { 148 | 149 | foreach($feeds as $f) { 150 | $f->name = $prefix.$f->name; 151 | if (!isset($f->tag)) { 152 | $f->tag = $nodeid; 153 | } 154 | 155 | $feedid = $this->feed->exists_tag_name($userid, $f->tag, $f->name); 156 | if ($feedid == false) { 157 | $f->action = 'create'; 158 | $f->id = -1; 159 | } 160 | else { 161 | $f->action = 'none'; 162 | $f->id = $feedid; 163 | } 164 | } 165 | } 166 | 167 | protected function prepare_inputs($userid, $nodeid, $prefix, &$inputs) { 168 | 169 | foreach($inputs as $i) { 170 | $i->name = $prefix.$i->name; 171 | if(!isset($i->node)) { 172 | $i->node = $nodeid; 173 | } 174 | 175 | $inputid = $this->input->exists_nodeid_name($userid, $i->node, $i->name); 176 | if ($inputid == false) { 177 | $i->action = 'create'; 178 | $i->id = -1; 179 | } 180 | else { 181 | $i->action = 'none'; 182 | $i->id = $inputid; 183 | } 184 | } 185 | } 186 | 187 | // Prepare the input process lists 188 | protected function prepare_input_processes($userid, $prefix, $feeds, &$inputs) { 189 | 190 | $process_list = $this->process->get_process_list(); // emoncms supported processes 191 | 192 | foreach($inputs as $i) { 193 | // for each input 194 | if (isset($i->id) && (isset($i->processList) || isset($i->processlist))) { 195 | $processes = isset($i->processList) ? $i->processList : $i->processlist; 196 | if (!empty($processes)) { 197 | $processes = $this->prepare_processes($prefix, $feeds, $inputs, $processes, $process_list); 198 | if (isset($i->action) && $i->action != 'create') { 199 | $processes_input = $this->input->get_processlist($i->id); 200 | if (!isset($processes['success'])) { 201 | if ($processes_input == '' && $processes != '') { 202 | $i->action = 'set'; 203 | } 204 | else if ($processes_input != $processes) { 205 | $i->action = 'override'; 206 | } 207 | } 208 | else { 209 | if ($processes_input == '') { 210 | $i->action = 'set'; 211 | } 212 | else { 213 | $i->action = 'override'; 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | // Prepare the feed process lists 223 | protected function prepare_feed_processes($userid, $prefix, &$feeds, $inputs) { 224 | 225 | $process_list = $this->process->get_process_list(); // emoncms supported processes 226 | 227 | foreach($feeds as $f) { 228 | // for each feed 229 | if ($f->engine == Engine::VIRTUALFEED && isset($f->id) && (isset($f->processList) || isset($f->processlist))) { 230 | $processes = isset($f->processList) ? $f->processList : $f->processlist; 231 | if (!empty($processes)) { 232 | $processes = $this->prepare_processes($prefix, $feeds, $inputs, $processes, $process_list); 233 | if (isset($f->action) && $f->action != 'create') { 234 | $processes_input = $this->feed->get_processlist($f->id); 235 | if (!isset($processes['success'])) { 236 | if ($processes_input == '' && $processes != '') { 237 | $f->action = 'set'; 238 | } 239 | else if ($processes_input != $processes) { 240 | $f->action = 'override'; 241 | } 242 | } 243 | else { 244 | if ($processes_input == '') { 245 | $f->action = 'set'; 246 | } 247 | else { 248 | $f->action = 'override'; 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | // Prepare template processes 258 | protected function prepare_processes($prefix, $feeds, $inputs, &$processes, $process_list) { 259 | $process_list_by_func = array(); 260 | foreach ($process_list as $process_id => $process_item) { 261 | $func = $process_item['function']; 262 | $process_list_by_func[$func] = $process_id; 263 | } 264 | $processes_converted = array(); 265 | 266 | $failed = false; 267 | foreach($processes as &$process) { 268 | // If process names are used map to process id 269 | if (isset($process_list_by_func[$process->process])) $process->process = $process_list_by_func[$process->process]; 270 | 271 | if (!isset($process_list[$process->process])) { 272 | $this->log->error("prepare_processes() Process '$process->process' not supported. Module missing?"); 273 | return array('success'=>false, 'message'=>"Process '$process->process' not supported. Module missing?"); 274 | } 275 | $process->name = $process_list[$process->process]['name']; 276 | $process->short = $process_list[$process->process]['short']; 277 | 278 | // Arguments 279 | if(isset($process->arguments)) { 280 | if(isset($process->arguments->type)) { 281 | $process->arguments->type = @constant($process->arguments->type); // ProcessArg:: 282 | $process_type = $process_list[$process->process]['argtype']; // get emoncms process ProcessArg 283 | 284 | if ($process_type != $process->arguments->type) { 285 | $this->log->error("prepare_processes() Bad device template. Missmatch ProcessArg type. Got '$process->arguments->type' expected '$process_type'. process='$process->process'"); 286 | return array('success'=>false, 'message'=>"Bad device template. Missmatch ProcessArg type. Got '$process->arguments->type' expected '$process_type'. process='$process->process'"); 287 | } 288 | else if ($process->arguments->type === ProcessArg::INPUTID || $process->arguments->type === ProcessArg::FEEDID) { 289 | $process->arguments->value = $prefix.$process->arguments->value; 290 | } 291 | 292 | $result = $this->convert_process($feeds, $inputs, $process, $process_list); 293 | if (isset($result['success'])) { 294 | $failed = true; 295 | } 296 | else { 297 | $processes_converted[] = $result; 298 | } 299 | } 300 | else { 301 | $this->log->error("prepare_processes() Bad device template. Argument type is missing, set to NONE if not required. process='$process->process' type='".$process->arguments->type."'"); 302 | return array('success'=>false, 'message'=>"Bad device template. Argument type is missing, set to NONE if not required. process='$process->process' type='".$process->arguments->type."'"); 303 | } 304 | } 305 | else { 306 | $this->log->error("prepare_processes() Bad device template. Missing processList arguments. process='$process->process'"); 307 | return array('success'=>false, 'message'=>"Bad device template. Missing processList arguments. process='$process->process'"); 308 | } 309 | } 310 | if (!$failed) { 311 | return implode(",", $processes_converted); 312 | } 313 | return array('success'=>false, 'message'=>"Unable to convert all prepared processes"); 314 | } 315 | 316 | // Create the feeds 317 | protected function create_feeds($userid, &$feeds) { 318 | 319 | foreach($feeds as $f) { 320 | $datatype = constant($f->type); // DataType:: 321 | $engine = constant($f->engine); // Engine:: 322 | if (isset($f->unit)) $unit = $f->unit; else $unit = ""; 323 | 324 | $options = new stdClass(); 325 | if (property_exists($f, "interval")) { 326 | $options->interval = $f->interval; 327 | } 328 | 329 | if ($f->action === 'create') { 330 | $this->log->info("create_feeds() userid=$userid tag=$f->tag name=$f->name datatype=$datatype engine=$engine unit=$unit"); 331 | 332 | $result = $this->feed->create($userid,$f->tag,$f->name,$datatype,$engine,$options,$unit); 333 | if($result['success'] !== true) { 334 | $this->log->error("create_feeds() failed for userid=$userid tag=$f->tag name=$f->name datatype=$datatype engine=$engine unit=$unit"); 335 | } 336 | else { 337 | $f->id = $result["feedid"]; // Assign the created feed id to the feeds array 338 | } 339 | } 340 | } 341 | } 342 | 343 | // Create the inputs 344 | protected function create_inputs($userid, &$inputs) { 345 | 346 | foreach($inputs as $i) { 347 | if ($i->action === 'create') { 348 | $this->log->info("create_inputs() userid=$userid nodeid=$i->node name=$i->name description=$i->description"); 349 | 350 | $inputid = $this->input->create_input($userid, $i->node, $i->name); 351 | if(!$this->input->exists($inputid)) { 352 | $this->log->error("create_inputs() failed for userid=$userid nodeid=$i->node name=$i->name description=$i->description"); 353 | } 354 | else { 355 | $this->input->set_fields($inputid, '{"description":"'.$i->description.'"}'); 356 | $i->id = $inputid; // Assign the created input id to the inputs array 357 | } 358 | } 359 | } 360 | } 361 | 362 | // Create the input process lists 363 | protected function create_input_processes($userid, $feeds, $inputs) { 364 | 365 | $process_list = $this->process->get_process_list(); // emoncms supported processes 366 | 367 | foreach($inputs as $i) { 368 | if ($i->action !== 'none') { 369 | if (isset($i->id) && (isset($i->processList) || isset($i->processlist))) { 370 | $processes = isset($i->processList) ? $i->processList : $i->processlist; 371 | $inputid = $i->id; 372 | 373 | if (is_array($processes)) { 374 | $processes_converted = array(); 375 | 376 | $failed = false; 377 | foreach($processes as $process) { 378 | $result = $this->convert_process($feeds, $inputs, $process, $process_list); 379 | if (isset($result['success']) && !$result['success']) { 380 | $failed = true; 381 | break; 382 | } 383 | $processes_converted[] = $result; 384 | } 385 | $processes = implode(",", $processes_converted); 386 | if (!$failed && $processes != "") { 387 | $this->log->info("create_inputs_processes() calling input->set_processlist inputid=$inputid processes=$processes"); 388 | $this->input->set_processlist($userid, $inputid, $processes, $process_list); 389 | } 390 | } 391 | } 392 | } 393 | } 394 | } 395 | 396 | // Create the feed process lists 397 | protected function create_feed_processes($userid, $feeds, $inputs) { 398 | 399 | $process_list = $this->process->get_process_list(); // emoncms supported processes 400 | 401 | foreach($feeds as $f) { 402 | if ($f->action !== 'none') { 403 | if ($f->engine == Engine::VIRTUALFEED && isset($f->id) && (isset($f->processList) || isset($f->processlist))) { 404 | $processes = isset($f->processList) ? $f->processList : $f->processlist; 405 | $feedid = $f->id; 406 | 407 | if (is_array($processes)) { 408 | $processes_converted = array(); 409 | 410 | $failed = false; 411 | foreach($processes as $process) { 412 | $result = $this->convert_process($feeds, $inputs, $process, $process_list); 413 | if (isset($result['success']) && !$result['success']) { 414 | $failed = true; 415 | break; 416 | } 417 | $processes_converted[] = $result; 418 | } 419 | $processes = implode(",", $processes_converted); 420 | if (!$failed && $processes != "") { 421 | $this->log->info("create_feeds_processes() calling feed->set_processlist feedId=$feedid processes=$processes"); 422 | $this->feed->set_processlist($userid, $feedid, $processes, $process_list); 423 | } 424 | } 425 | } 426 | } 427 | } 428 | } 429 | 430 | // Converts template process 431 | protected function convert_process($feeds, $inputs, $process, $process_list) { 432 | if (isset($process->arguments->value)) { 433 | $value = $process->arguments->value; 434 | } 435 | else if ($process->arguments->type === ProcessArg::NONE) { 436 | $value = 0; 437 | } 438 | else { 439 | $this->log->error("convertProcess() Bad device template. Undefined argument value. process='$process->process' type='".$process->arguments->type."'"); 440 | return array('success'=>false, 'message'=>"Bad device template. Undefined argument value. process='$process->process' type='".$process->arguments->type."'"); 441 | } 442 | 443 | if ($process->arguments->type === ProcessArg::VALUE) { 444 | } 445 | else if ($process->arguments->type === ProcessArg::INPUTID) { 446 | $temp = $this->search_array($inputs, 'name', $value); // return input array that matches $inputArray[]['name']=$value 447 | if (isset($temp->id) && $temp->id > 0) { 448 | $value = $temp->id; 449 | } 450 | else { 451 | $this->log->info("convertProcess() Input name '$value' was not found. process='$process->process' type='".$process->arguments->type."'"); 452 | return array('success'=>false, 'message'=>"Input name '$value' was not found. process='$process->process' type='".$process->arguments->type."'"); 453 | } 454 | } 455 | else if ($process->arguments->type === ProcessArg::FEEDID) { 456 | $temp = $this->search_array($feeds, 'name', $value); // return feed array that matches $feedArray[]['name']=$value 457 | if (isset($temp->id) && $temp->id > 0) { 458 | $value = $temp->id; 459 | } 460 | else { 461 | $this->log->info("convertProcess() Feed name '$value' was not found. process='$process->process' type='".$process->arguments->type."'"); 462 | return array('success'=>false, 'message'=>"Feed name '$value' was not found. process='$process->process' type='".$process->arguments->type."'"); 463 | } 464 | } 465 | else if ($process->arguments->type === ProcessArg::NONE) { 466 | $value = ""; 467 | } 468 | else if ($process->arguments->type === ProcessArg::TEXT) { 469 | } 470 | else if ($process->arguments->type === ProcessArg::SCHEDULEID) { 471 | //not supporte for now 472 | } 473 | else { 474 | $this->log->error("convertProcess() Bad device template. Unsuported argument type. process='$process->process' type='".$process->arguments->type."'"); 475 | return array('success'=>false, 'message'=>"Bad device template. Unsuported argument type. process='$process->process' type='".$process->arguments->type."'"); 476 | } 477 | 478 | if (isset($process_list[$process->process]['id_num'])) { 479 | $id = $process_list[$process->process]['id_num']; 480 | } 481 | else { 482 | $id = $process->process; 483 | } 484 | $this->log->info("convertProcess() process process='$id' type='".$process->arguments->type."' value='" . $value . "'"); 485 | return $id.":".$value; 486 | } 487 | 488 | protected function parse_prefix($nodeid, $name, $template) { 489 | if (isset($template->prefix)) { 490 | $prefix = $template->prefix; 491 | if ($prefix === "node") { 492 | return strtolower($nodeid)."_"; 493 | } 494 | else if ($prefix === "name") { 495 | return strtolower($name)."_"; 496 | } 497 | } 498 | return ""; 499 | } 500 | 501 | protected function search_array($array, $key, $val) { 502 | foreach ($array as $item) { 503 | if (isset($item->$key) && $item->$key == $val) { 504 | return $item; 505 | } 506 | } 507 | return null; 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /Views/device_dialog.js: -------------------------------------------------------------------------------- 1 | var device_dialog = 2 | { 3 | templates: null, 4 | deviceTemplate: null, 5 | deviceType: null, 6 | device: null, 7 | 8 | 'loadConfig':function(templates, device) { 9 | this.templates = templates; 10 | 11 | if (device != null) { 12 | this.deviceTemplate = null; 13 | this.deviceType = device.type; 14 | this.device = device; 15 | } 16 | else { 17 | this.deviceTemplate = null; 18 | this.deviceType = null; 19 | this.device = null; 20 | } 21 | 22 | this.drawConfig(); 23 | }, 24 | 25 | 'drawConfig':function() { 26 | $("#device-config-modal").modal('show'); 27 | this.adjustConfigModal(); 28 | this.clearConfigModal(); 29 | 30 | var categories = []; 31 | var devicesByCategory = {}; 32 | for (var id in this.templates) { 33 | var device = this.templates[id]; 34 | device['id'] = id; 35 | 36 | if (devicesByCategory[device.category] == undefined) { 37 | devicesByCategory[device.category] = []; 38 | categories.push(device.category); 39 | } 40 | devicesByCategory[device.category].push(device); 41 | } 42 | // Place OpenEnergyMonitor prominently at first place, while sorting other categories 43 | if (categories.indexOf('OpenEnergyMonitor') > -1) { 44 | categories.splice(categories.indexOf('OpenEnergyMonitor'), 1); 45 | categories.sort() 46 | categories = ['OpenEnergyMonitor'].concat(categories); 47 | } 48 | 49 | for (var i in categories) { 50 | var category = categories[i]; 51 | var categoryid = category.replace(/\W/g, '').toLowerCase(); 52 | 53 | var groups = []; 54 | var devicesByGroup = {}; 55 | for (var i in devicesByCategory[category]) { 56 | var group = devicesByCategory[category][i].group; 57 | if (devicesByGroup[group] == undefined) { 58 | devicesByGroup[group] = []; 59 | groups.push(group); 60 | } 61 | devicesByGroup[group].push(devicesByCategory[category][i]); 62 | } 63 | groups.sort(); 64 | 65 | $('#template-list').append( 66 | "
" + 67 | "
" + 68 | "" + 70 | category + 71 | "" + 72 | "
" + 73 | "
" + 74 | "
" + 75 | "
" + 76 | "
" + 77 | "
" + 78 | "
" 79 | ); 80 | 81 | for (var i in groups) { 82 | var group = groups[i]; 83 | var groupid = group.replace(/\W/g, '').toLowerCase(); 84 | $('#template-'+categoryid).append( 85 | "
" + 86 | "
" + 87 | "" + 89 | group + 90 | "" + 91 | "
" + 92 | "
" + 93 | "
" + 94 | "
" + 95 | "
" 96 | ); 97 | var body = $('#template-'+categoryid+'-'+groupid); 98 | 99 | for (var i in devicesByGroup[group]) { 100 | var id = devicesByGroup[group][i].id; 101 | var name = devicesByGroup[group][i].name; 102 | if (name.length > 25) { 103 | name = name.substr(0, 25) + "..."; 104 | } 105 | 106 | body.append( 107 | "
" + 109 | ""+name+"" + 110 | "
" 111 | ); 112 | } 113 | } 114 | } 115 | 116 | if (this.deviceType != null && this.deviceType != '') { 117 | if (this.templates[this.deviceType]!=undefined) { 118 | var template = this.templates[this.deviceType]; 119 | var category = template.category.replace(/\W/g, '').toLowerCase(); 120 | var group = template.group.replace(/\W/g, '').toLowerCase(); 121 | var id = this.deviceType.replace('/', '-'); 122 | 123 | $("#template-"+category+"-collapse").collapse('show'); 124 | $("#template-"+category+"-"+group+"-collapse").collapse('show'); 125 | $("#template-"+category+"-"+group+"-"+id).addClass("device-selected"); 126 | 127 | $('#template-description').html(''+template.description+''); 128 | $('#template-info').show(); 129 | } 130 | } 131 | 132 | // Initialize callbacks 133 | this.registerConfigEvents(); 134 | }, 135 | 136 | 'clearConfigModal':function() { 137 | $("#template-list").text(''); 138 | 139 | var tooltip = "Defaults, like inputs and associated feeds will be automaticaly configured together with the device.
" + 140 | "Initializing a device usualy should only be done once on installation. " + 141 | "If the configuration was already applied, only missing inputs and feeds will be created."; 142 | 143 | $('#template-tooltip').attr("title", tooltip).tooltip({html: true}); 144 | 145 | if (this.device != null) { 146 | $('#device-config-node').val(this.device.nodeid); 147 | $('#device-config-name').val(this.device.name); 148 | $('#device-config-description').val(this.device.description); 149 | $('#device-config-devicekey').val(this.device.devicekey).prop("disabled", false); 150 | $("#device-config-devicekey-new").prop("disabled", false); 151 | $('#device-delete').show(); 152 | $("#device-save").html("Save"); 153 | if (this.device.type != null && this.device.type != '') { 154 | $("#device-init").show(); 155 | $('#select-device-alert').addClass('hidden') 156 | } else { 157 | $("#device-init").hide(); 158 | $('#device-sidebar').addClass('show') 159 | $('#select-device-alert').removeClass('hidden') 160 | } 161 | } 162 | else { 163 | $('#device-config-node').val(''); 164 | $('#device-config-name').val(''); 165 | $('#device-config-description').val(''); 166 | $('#device-config-devicekey').val('').prop("disabled", true); 167 | $("#device-config-devicekey-new").prop("disabled", true); 168 | $('#device-delete').hide(); 169 | $("#device-init").hide(); 170 | $("#device-save").html("Save & Initialize"); 171 | } 172 | device_dialog.drawTemplate(); 173 | }, 174 | 175 | 'adjustConfigModal':function() { 176 | 177 | var width = $(window).width(); 178 | var height = $(window).height(); 179 | 180 | if ($("#device-config-modal").length) { 181 | var h = height - $("#device-config-modal").position().top - 180; 182 | $("#device-config-body").height(h); 183 | } 184 | 185 | if (width < 680) { 186 | $("#device-sidebar-open").show(); 187 | $("#device-sidebar-close").show(); 188 | $("#device-sidebar").removeClass('show') 189 | } else { 190 | $("#device-sidebar-open").hide(); 191 | $("#device-sidebar-close").hide(); 192 | $("#device-sidebar").addClass('show') 193 | } 194 | }, 195 | 196 | 'registerConfigEvents':function() { 197 | 198 | $("#template-list").off('click').on('click', '.group-device', function () { 199 | var type = $(this).data("type"); 200 | 201 | $(".group-device[data-type='"+device_dialog.deviceType+"']").removeClass("device-selected"); 202 | if (device_dialog.deviceType !== type) { 203 | $(this).addClass("device-selected"); 204 | device_dialog.deviceType = type; 205 | 206 | var template = device_dialog.templates[type]; 207 | $('#template-description').html(''+template.description+''); 208 | $('#template-info').show(); 209 | $("#device-init").hide(); 210 | } 211 | else { 212 | device_dialog.deviceType = null; 213 | 214 | $('#template-description').text(''); 215 | $('#template-info').hide(); 216 | $("#device-init").show() 217 | } 218 | if ($(window).width() < 680) { 219 | $("#device-sidebar").removeClass('show') 220 | } 221 | 222 | device_dialog.drawTemplate(); 223 | }); 224 | 225 | $("#device-sidebar-open").off('click').on('click', function () { 226 | $("#device-sidebar").addClass('show') 227 | }); 228 | 229 | $("#device-sidebar-close").off('click').on('click', function () { 230 | $("#device-sidebar").removeClass('show') 231 | }); 232 | 233 | $("#device-save").off('click').on('click', function () { 234 | 235 | var node = $('#device-config-node').val(); 236 | var name = $('#device-config-name').val(); 237 | 238 | if (name && node) { 239 | var desc = $('#device-config-description').val(); 240 | var devicekey = $('#device-config-devicekey').val(); 241 | 242 | var init = false; 243 | if (device_dialog.device != null) { 244 | var fields = {}; 245 | if (device_dialog.device.nodeid != node) fields['nodeid'] = node; 246 | if (device_dialog.device.name != name) fields['name'] = name; 247 | if (device_dialog.device.description != desc) fields['description'] = desc; 248 | if (device_dialog.device.devicekey != devicekey) fields['devicekey'] = devicekey; 249 | 250 | if (device_dialog.device.type != device_dialog.deviceType) { 251 | if (device_dialog.deviceType != null) { 252 | fields['type'] = device_dialog.deviceType; 253 | init = true; 254 | } 255 | else fields['type'] = ''; 256 | } 257 | 258 | var result = device.set(device_dialog.device.id, fields); 259 | if (typeof result.success !== 'undefined' && !result.success) { 260 | alert('Unable to update device fields:\n'+result.message); 261 | return false; 262 | } 263 | update(); 264 | } 265 | else { 266 | var type = device_dialog.deviceType; 267 | var result = device.create(node, name, desc, type); 268 | if (typeof result.success !== 'undefined' && !result.success) { 269 | alert('Unable to create device:\n'+result.message); 270 | return false; 271 | } 272 | if (type != null) { 273 | device_dialog.device = { 274 | id: result, 275 | nodeid: node, 276 | name: name, 277 | type: type 278 | }; 279 | init = true; 280 | } 281 | update(); 282 | } 283 | $('#device-config-modal').modal('hide'); 284 | if (init) device_dialog.loadInit(); 285 | } 286 | else { 287 | alert('Device needs to be configured first.'); 288 | return false; 289 | } 290 | }); 291 | 292 | $("#device-delete").off('click').on('click', function () { 293 | $('#device-config-modal').modal('hide'); 294 | device_dialog.loadDelete(device_dialog.device, null); 295 | }); 296 | 297 | $("#device-init").off('click').on('click', function () { 298 | $('#device-config-modal').modal('hide'); 299 | device_dialog.loadInit(); 300 | }); 301 | 302 | $("#device-config-devicekey-new").off('click').on('click', function () { 303 | device_dialog.device.devicekey = device.setNewDeviceKey(device_dialog.device.id); 304 | $('#device-config-devicekey').val(device_dialog.device.devicekey); 305 | }); 306 | }, 307 | 308 | 'drawTemplate':function() { 309 | if (device_dialog.deviceType == null || device_dialog.deviceType == "" || !device_dialog.deviceType in device_dialog.templates) { 310 | $('#template-description').text(''); 311 | $('#template-info').hide(); 312 | return; 313 | } 314 | 315 | // console.log("deviceType:"+device_dialog.deviceType) 316 | // console.log(device_dialog.templates) 317 | var template = device_dialog.templates[device_dialog.deviceType]; 318 | if (template!=undefined) { 319 | $('#template-description').html(''+template.description+''); 320 | } 321 | $('#template-info').show(); 322 | }, 323 | 324 | 'loadInit': function() { 325 | var result = device.prepareTemplate(device_dialog.device.id); 326 | if (typeof result.success !== 'undefined' && !result.success) { 327 | alert('Unable to initialize device:\n'+result.message); 328 | return false; 329 | } 330 | device_dialog.deviceTemplate = result; 331 | device_dialog.drawInit(result); 332 | 333 | // Initialize callbacks 334 | $("#device-init-confirm").off('click').on('click', function() { 335 | $('#device-init-modal').modal('hide'); 336 | 337 | var template = device_dialog.parseTemplate(); 338 | var result = device.init(device_dialog.device.id, template); 339 | if (typeof result.success !== 'undefined' && !result.success) { 340 | alert('Unable to initialize device:\n'+result.message); 341 | return false; 342 | } 343 | 344 | $('#wrap').trigger("device-init"); 345 | }); 346 | }, 347 | 348 | 'drawInit': function (result) { 349 | $('#device-init-modal').modal('show'); 350 | device_dialog.adjustInitModal(); 351 | 352 | $('#device-init-modal-label').html('Initialize Device: '+device_dialog.device.name+''); 353 | 354 | if (typeof result.feeds !== 'undefined' && result.feeds.length > 0) { 355 | $('#device-init-feeds').show(); 356 | var table = ""; 357 | for (var i = 0; i < result.feeds.length; i++) { 358 | var feed = result.feeds[i]; 359 | var row = ""; 360 | if (feed.action.toLowerCase() == "none") { 361 | row += ""; 362 | } 363 | else { 364 | row += ""; 365 | } 366 | row += ""+device_dialog.drawInitAction(feed.action)+"" 367 | row += ""+feed.tag+""+feed.name+""; 368 | row += ""+device_dialog.drawInitProcessList(feed.processList)+""; 369 | 370 | table += ""+row+""; 371 | } 372 | $('#device-init-feeds-table').html(table); 373 | } 374 | else { 375 | $('#device-init-feeds').hide(); 376 | } 377 | 378 | if (typeof result.inputs !== 'undefined' && result.inputs.length > 0) { 379 | $('#device-init-inputs').show(); 380 | var table = ""; 381 | for (var i = 0; i < result.inputs.length; i++) { 382 | var input = result.inputs[i]; 383 | var row = ""; 384 | if (input.action.toLowerCase() == "none") { 385 | row += ""; 386 | } 387 | else { 388 | row += ""; 389 | } 390 | row += ""+device_dialog.drawInitAction(input.action)+"" 391 | row += ""+input.node+""+input.name+""+input.description+""; 392 | row += ""+device_dialog.drawInitProcessList(input.processList)+""; 393 | 394 | table += ""+row+""; 395 | } 396 | $('#device-init-inputs-table').html(table); 397 | } 398 | else { 399 | $('#device-init-inputs').hide(); 400 | $('#device-init-inputs-table').html(""); 401 | } 402 | 403 | return true; 404 | }, 405 | 406 | 'drawInitAction': function (action) { 407 | action = action.toLowerCase(); 408 | 409 | var color; 410 | if (action === 'create' || action === 'set') { 411 | color = "rgb(0,110,205)"; 412 | } 413 | else if (action === 'override') { 414 | color = "rgb(255,125,20)"; 415 | } 416 | else { 417 | color = "rgb(50,200,50)"; 418 | action = "exists" 419 | } 420 | action = action.charAt(0).toUpperCase() + action.slice(1); 421 | 422 | return ""+action+""; 423 | }, 424 | 425 | 'drawInitProcessList': function (processList) { 426 | if (!processList || processList.length < 1) return ""; 427 | 428 | var out = ""; 429 | for (var i = 0; i < processList.length; i++) { 430 | var process = processList[i]; 431 | if (process['arguments'] != undefined && process['arguments']['type'] != undefined) { 432 | var title; 433 | var label; 434 | switch(process['arguments']['type']) { 435 | case 0: // VALUE 436 | label = "important"; 437 | title = "Value - "; 438 | break; 439 | 440 | case 1: //INPUTID 441 | label = "warning"; 442 | title = "Input - "; 443 | break; 444 | 445 | case 2: //FEEDID 446 | label = "info"; 447 | title = "Feed - "; 448 | break; 449 | 450 | case 3: // NONE 451 | label = "important"; 452 | title = ""; 453 | break; 454 | 455 | case 4: // TEXT 456 | label = "important"; 457 | title = "Text - "; 458 | break; 459 | 460 | case 5: // SCHEDULEID 461 | label = "warning"; 462 | title = "Schedule - " 463 | break; 464 | 465 | default: 466 | label = "important"; 467 | title = "ERROR - "; 468 | break; 469 | } 470 | title += process["name"]; 471 | 472 | if (process['arguments']['value'] != undefined) { 473 | title += ": " + process['arguments']['value']; 474 | } 475 | 476 | out += ""+process["short"]+" "; 477 | } 478 | } 479 | return out; 480 | }, 481 | 482 | 'adjustInitModal':function() { 483 | 484 | var width = $(window).width(); 485 | var height = $(window).height(); 486 | 487 | if ($("#device-init-modal").length) { 488 | var h = height - $("#device-init-modal").position().top - 180; 489 | $("#device-init-body").height(h); 490 | } 491 | }, 492 | 493 | 'parseTemplate': function() { 494 | var template = {}; 495 | 496 | template['feeds'] = []; 497 | if (typeof device_dialog.deviceTemplate.feeds !== 'undefined' && 498 | device_dialog.deviceTemplate.feeds.length > 0) { 499 | 500 | var feeds = device_dialog.deviceTemplate.feeds; 501 | $("#device-init-feeds-table tr").find('input[type="checkbox"]:checked').each(function() { 502 | template['feeds'].push(feeds[$(this).attr("row")]); 503 | }); 504 | } 505 | 506 | template['inputs'] = []; 507 | if (typeof device_dialog.deviceTemplate.inputs !== 'undefined' && 508 | device_dialog.deviceTemplate.inputs.length > 0) { 509 | 510 | var inputs = device_dialog.deviceTemplate.inputs; 511 | $("#device-init-inputs-table tr").find('input[type="checkbox"]:checked').each(function() { 512 | template['inputs'].push(inputs[$(this).attr("row")]); 513 | }); 514 | } 515 | 516 | return template; 517 | }, 518 | 519 | 'loadDelete': function(device, tablerow) { 520 | this.device = device; 521 | 522 | $('#device-delete-modal').modal('show'); 523 | $('#device-delete-modal-label').html('Delete Device: '+device.name+''); 524 | 525 | // Initialize callbacks 526 | this.registerDeleteEvents(tablerow); 527 | }, 528 | 529 | 'registerDeleteEvents':function(row) { 530 | 531 | $("#device-delete-confirm").off('click').on('click', function() { 532 | device.remove(device_dialog.device.id); 533 | if (row != null) { 534 | table.remove(row); 535 | update(); 536 | } 537 | else if (typeof device_dialog.device.inputs !== 'undefined') { 538 | // If the table row is undefined and an input list exists, the config dialog 539 | // was opened in the input view and all corresponding inputs will be deleted 540 | var inputIds = []; 541 | for (var i in device_dialog.device.inputs) { 542 | var inputId = device_dialog.device.inputs[i].id; 543 | inputIds.push(parseInt(inputId)); 544 | } 545 | input.delete_multiple_async(inputIds) 546 | .done(function(){ 547 | update(); 548 | }); 549 | } 550 | $('#device-delete-modal').modal('hide'); 551 | $('#wrap').trigger("device-delete"); 552 | }); 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /device_model.php: -------------------------------------------------------------------------------- 1 | mysqli = $mysqli; 26 | $this->redis = $redis; 27 | $this->templates = array(); 28 | $this->log = new EmonLogger(__FILE__); 29 | } 30 | 31 | public function devicekey_session($devicekey) { 32 | // 1. Only allow alphanumeric characters 33 | // if (!ctype_alnum($devicekey)) return array(); 34 | 35 | // 2. Only allow 32 character length 36 | if (strlen($devicekey)!=32) return array(); 37 | 38 | $session = array(); 39 | $time = time(); 40 | 41 | //---------------------------------------------------- 42 | // Check for devicekey login 43 | //---------------------------------------------------- 44 | if($this->redis && $this->redis->exists("device:key:$devicekey")) { 45 | $session['userid'] = $this->redis->get("device:key:$devicekey:user"); 46 | $session['read'] = 1; 47 | $session['write'] = 1; 48 | $session['admin'] = 0; 49 | $session['lang'] = "en"; // API access is always in english 50 | $session['username'] = "API"; 51 | $session['deviceid'] = $this->redis->get("device:key:$devicekey:device"); 52 | $session['nodeid'] = $this->redis->get("device:key:$devicekey:node"); 53 | $this->redis->hMset("device:lastvalue:".$session['device'], array('time' => $time)); 54 | } 55 | else { 56 | $stmt = $this->mysqli->prepare("SELECT id, userid, nodeid FROM device WHERE devicekey=?"); 57 | $stmt->bind_param("s",$devicekey); 58 | $stmt->execute(); 59 | $stmt->bind_result($id,$userid,$nodeid); 60 | $result = $stmt->fetch(); 61 | $stmt->close(); 62 | 63 | if ($result && $id>0) { 64 | $session['userid'] = $userid; 65 | $session['read'] = 1; 66 | $session['write'] = 1; 67 | $session['admin'] = 0; 68 | $session['lang'] = "en"; // API access is always in english 69 | $session['username'] = "API"; 70 | $session['deviceid'] = $id; 71 | $session['nodeid'] = $nodeid; 72 | 73 | if ($this->redis) { 74 | $this->redis->set("device:key:$devicekey:user",$userid); 75 | $this->redis->set("device:key:$devicekey:device",$id); 76 | $this->redis->set("device:key:$devicekey:node",$nodeid); 77 | $this->redis->hMset("device:lastvalue:$id", array('time' => $time)); 78 | } else { 79 | //$time = date("Y-n-j H:i:s", $time); 80 | $this->mysqli->query("UPDATE device SET time='$time' WHERE id = '$id"); 81 | } 82 | } 83 | } 84 | 85 | return $session; 86 | } 87 | 88 | public function exist($id) { 89 | static $device_exists_cache = array(); // Array to hold the cache 90 | if (isset($device_exists_cache[$id])) { 91 | $device_exist = $device_exists_cache[$id]; // Retrieve from static cache 92 | } 93 | else { 94 | $device_exist = false; 95 | if ($this->redis) { 96 | if (!$this->redis->exists("device:$id")) { 97 | if ($this->load_device_to_redis($id)) { 98 | $device_exist = true; 99 | } 100 | } 101 | else { 102 | $device_exist = true; 103 | } 104 | } 105 | else { 106 | $id = (int) $id; 107 | $result = $this->mysqli->query("SELECT id FROM device WHERE id = '$id'"); 108 | if ($result->num_rows > 0) $device_exist = true; 109 | } 110 | $device_exists_cache[$id] = $device_exist; // Cache it 111 | } 112 | return $device_exist; 113 | } 114 | 115 | public function exists_name($userid, $name) { 116 | $userid = intval($userid); 117 | $name = preg_replace('/[^\p{L}_\p{N}\s\-:]/u','',$name); 118 | 119 | $stmt = $this->mysqli->prepare("SELECT id,name FROM device WHERE userid=? AND name=?"); 120 | $stmt->bind_param("is", $userid, $name); 121 | $stmt->execute(); 122 | $stmt->bind_result($id,$_name); 123 | $result = $stmt->fetch(); 124 | $stmt->close(); 125 | 126 | // SQL search may not be case sensitive 127 | if ($_name!=$name) return false; 128 | 129 | if ($result && $id > 0) return $id; else return false; 130 | } 131 | 132 | public function exists_nodeid($userid, $nodeid) { 133 | $userid = intval($userid); 134 | $nodeid = preg_replace('/[^\p{L}_\p{N}\s\-:]/u','',$nodeid); 135 | 136 | $stmt = $this->mysqli->prepare("SELECT id,nodeid FROM device WHERE userid=? AND nodeid=?"); 137 | $stmt->bind_param("is", $userid, $nodeid); 138 | $stmt->execute(); 139 | $stmt->bind_result($id,$_nodeid); 140 | $result = $stmt->fetch(); 141 | $stmt->close(); 142 | 143 | // SQL search may not be case sensitive 144 | if ($_nodeid!=$nodeid) return false; 145 | 146 | if ($result && $id > 0) return $id; else return false; 147 | } 148 | 149 | public function request_auth($ip) { 150 | if (!$this->redis) { 151 | return array("success"=>false, "message"=>"Unable to handle authentication requests without redis"); 152 | } 153 | $ip_parts = explode(".", $ip); 154 | for ($i=0; $iredis->get("device:auth:allow"); 158 | // Only show authentication details to allowed ip address 159 | if ($allow_ip == $ip) { 160 | $this->redis->del("device:auth:allow"); 161 | global $settings; 162 | return $settings['mqtt']['user'].":".$settings['mqtt']['password'].":".$settings['mqtt']['basetopic']; 163 | } else { 164 | $this->redis->set("device:auth:request", json_encode(array("ip"=>$ip))); 165 | return array("success"=>true, "message"=>"Authentication request registered for IP $ip"); 166 | } 167 | } 168 | 169 | public function get_auth_request() { 170 | if (!$this->redis) { 171 | return array("success"=>false, "message"=>"Unable to handle authentication requests without redis"); 172 | } 173 | if ($device_auth = $this->redis->get("device:auth:request")) { 174 | $device_auth = json_decode($device_auth); 175 | return array_merge(array("success"=>true, "ip"=>$device_auth->ip)); 176 | } else { 177 | return array("success"=>true, "message"=>"No authentication request registered"); 178 | } 179 | } 180 | 181 | public function allow_auth_request($ip) { 182 | if (!$this->redis) { 183 | return array("success"=>false, "message"=>"Unable to handle authentication requests without redis"); 184 | } 185 | $ip_parts = explode(".", $ip); 186 | for ($i=0; $iredis->set("device:auth:allow", $ip); // Temporary availability of auth for device ip address 190 | $this->redis->expire("device:auth:allow", 60); // Expire after 60 seconds 191 | $this->redis->del("device:auth:request"); 192 | 193 | return array("success"=>true, "message"=>"Authentication request allowed for IP $ip"); 194 | } 195 | 196 | public function get($id) { 197 | $id = intval($id); 198 | if (!$this->exist($id)) { 199 | if (!$this->redis || !$this->load_device_to_redis($id)) { 200 | return array('success'=>false, 'message'=>'Device does not exist'); 201 | } 202 | } 203 | 204 | if ($this->redis) { 205 | // Get from redis cache 206 | $device = (array) $this->redis->hGetAll("device:$id"); 207 | // Verify, if the cached device contains the userid, to avoid compatibility issues 208 | // with former versions where the userid was not cached. 209 | if (!isset($device['userid'])) { 210 | $this->load_device_to_redis($id); 211 | $device = $this->get($id); 212 | } 213 | $device['time'] = $this->redis->hget("device:lastvalue:".$id, 'time'); 214 | } 215 | else { 216 | // Get from mysql db 217 | $result = $this->mysqli->query("SELECT `id`,`userid`,`nodeid`,`name`,`description`,`type`,`devicekey`,`time` FROM device WHERE id = '$id'"); 218 | $device = (array) $result->fetch_object(); 219 | } 220 | return $device; 221 | } 222 | 223 | public function get_list($userid) { 224 | if ($this->redis) { 225 | return $this->get_list_redis($userid); 226 | } else { 227 | return $this->get_list_mysql($userid); 228 | } 229 | } 230 | 231 | private function get_list_redis($userid) { 232 | $userid = intval($userid); 233 | 234 | if (!$this->redis->exists("user:device:$userid")) { 235 | $this->load_list_to_redis($userid); 236 | } 237 | 238 | $devices = array(); 239 | $deviceids = $this->redis->sMembers("user:device:$userid"); 240 | foreach ($deviceids as $id) { 241 | $device = $this->redis->hGetAll("device:$id"); 242 | // Verify, if the cached device contains the userid, to avoid compatibility issues 243 | // with former versions where the userid was not cached. 244 | if (!isset($device['userid'])) { 245 | $this->load_device_to_redis($id); 246 | $device = $this->get($id); 247 | } 248 | $device['time'] = $this->redis->hget("device:lastvalue:".$id, 'time'); 249 | $devices[] = $device; 250 | } 251 | usort($devices, function($d1, $d2) { 252 | if($d1['nodeid'] == $d2['nodeid']) 253 | return strcmp($d1['name'], $d2['name']); 254 | return strcmp($d1['nodeid'], $d2['nodeid']); 255 | }); 256 | return $devices; 257 | } 258 | 259 | private function get_list_mysql($userid) { 260 | $userid = intval($userid); 261 | 262 | $devices = array(); 263 | $result = $this->mysqli->query("SELECT `id`,`userid`,`nodeid`,`name`,`description`,`type`,`devicekey`,`time` FROM device WHERE userid = '$userid' ORDER BY nodeid, name asc"); 264 | while ($device = (array) $result->fetch_object()) { 265 | $devices[] = $device; 266 | } 267 | return $devices; 268 | } 269 | 270 | private function load_list_to_redis($userid) { 271 | $userid = intval($userid); 272 | 273 | $result = $this->mysqli->query("SELECT `id`,`userid`,`nodeid`,`name`,`description`,`type`,`devicekey` FROM device WHERE userid = '$userid'"); 274 | while ($row = $result->fetch_object()) { 275 | $this->redis->sAdd("user:device:$userid", $row->id); 276 | $this->redis->hMSet("device:".$row->id, array( 277 | 'id'=>$row->id, 278 | 'userid'=>$row->userid, 279 | 'nodeid'=>$row->nodeid, 280 | 'name'=>$row->name, 281 | 'description'=>$row->description, 282 | 'type'=>$row->type, 283 | 'devicekey'=>$row->devicekey 284 | )); 285 | } 286 | } 287 | 288 | private function load_device_to_redis($id) { 289 | $id = intval($id); 290 | 291 | $result = $this->mysqli->query("SELECT `id`,`userid`,`nodeid`,`name`,`description`,`type`,`devicekey` FROM device WHERE id = '$id'"); 292 | $row = $result->fetch_object(); 293 | if (!$row) { 294 | $this->log->warn("Device model: Requested device does not exist for id=$id"); 295 | return false; 296 | } 297 | $this->redis->hMSet("device:".$row->id, array( 298 | 'id'=>$row->id, 299 | 'userid'=>$row->userid, 300 | 'nodeid'=>$row->nodeid, 301 | 'name'=>$row->name, 302 | 'description'=>$row->description, 303 | 'type'=>$row->type, 304 | 'devicekey'=>$row->devicekey 305 | )); 306 | return true; 307 | } 308 | 309 | public function autocreate($userid, $_nodeid, $_type) { 310 | $userid = intval($userid); 311 | 312 | $nodeid = preg_replace('/[^\p{L}_\p{N}\s\-:]/u','',$_nodeid); 313 | if ($_nodeid != $nodeid) return array("success"=>false, "message"=>"Invalid nodeid"); 314 | $type = preg_replace('/[^\/\|\,\w\s\-:]/','',$_type); 315 | if ($_type != $type) return array("success"=>false, "message"=>"Invalid type"); 316 | 317 | $name = "$nodeid:$type"; 318 | 319 | $deviceid = $this->exists_nodeid($userid, $nodeid); 320 | 321 | if (!$deviceid) { 322 | $this->log->info("Automatically create device for user=$userid, nodeid=$nodeid"); 323 | $deviceid = $this->create($userid, $nodeid, null, null, null); 324 | if (!$deviceid) return array("success"=>false, "message"=>"Device creation failed"); 325 | } 326 | 327 | $result = $this->set_fields($deviceid,json_encode(array("name"=>$name,"nodeid"=>$nodeid,"type"=>$type))); 328 | if ($result['success']==true) { 329 | return $this->init($deviceid,false); 330 | } else { 331 | return $result; 332 | } 333 | } 334 | 335 | public function create($userid, $nodeid, $name, $description, $type) { 336 | $userid = intval($userid); 337 | $nodeid = preg_replace('/[^\p{L}_\p{N}\s\-:]/u', '', $nodeid); 338 | 339 | if (isset($name)) { 340 | $name = preg_replace('/[^\p{L}_\p{N}\s\-:]/u', '', $name); 341 | } else { 342 | $name = $nodeid; 343 | } 344 | 345 | if (isset($description)) { 346 | $description = preg_replace('/[^\p{L}_\p{N}\s\-:]/u', '', $description); 347 | } else { 348 | $description = ''; 349 | } 350 | 351 | if (isset($type) && $type != 'null') { 352 | $type = preg_replace('/[^\/\|\,\w\s\-:]/','', $type); 353 | } else { 354 | $type = ''; 355 | } 356 | 357 | if (!$this->exists_nodeid($userid, $nodeid)) { 358 | // device key disabled by default 359 | $devicekey = ""; // md5(uniqid(mt_rand(), true)); 360 | 361 | $stmt = $this->mysqli->prepare("INSERT INTO device (userid,nodeid,name,description,type,devicekey) VALUES (?,?,?,?,?,?)"); 362 | $stmt->bind_param("isssss",$userid,$nodeid,$name,$description,$type,$devicekey); 363 | $result = $stmt->execute(); 364 | $stmt->close(); 365 | if (!$result) return array('success'=>false, 'message'=>_("Error creating device")); 366 | 367 | $deviceid = $this->mysqli->insert_id; 368 | 369 | if ($deviceid > 0) { 370 | // Add the device to redis 371 | if ($this->redis) { 372 | // Reload all devices from mysql here to ensure cache is not out of sync 373 | $this->load_list_to_redis($userid); 374 | $this->redis->sAdd("user:device:$userid", $deviceid); 375 | $this->redis->hMSet("device:".$deviceid, array( 376 | 'id'=>$deviceid, 377 | 'userid'=>$userid, 378 | 'nodeid'=>$nodeid, 379 | 'name'=>$name, 380 | 'description'=>$description, 381 | 'type'=>$type, 382 | 'devicekey'=>$devicekey 383 | )); 384 | } 385 | return $deviceid; 386 | } 387 | return array('success'=>false, 'result'=>"SQL returned invalid insert feed id"); 388 | } 389 | return array('success'=>false, 'message'=>'Device already exists'); 390 | } 391 | 392 | public function delete($id) { 393 | $id = intval($id); 394 | if (!$this->exist($id)) { 395 | if (!$this->redis || !$this->load_device_to_redis($id)) { 396 | return array('success'=>false, 'message'=>'Device does not exist'); 397 | } 398 | } 399 | 400 | $this->mysqli->query("DELETE FROM device WHERE `id` = '$id'"); 401 | if (isset($device_exists_cache[$id])) { unset($device_exists_cache[$id]); } // Clear static cache 402 | 403 | if ($this->redis) { 404 | $userid = $this->redis->hget("device:$id",'userid'); 405 | if (isset($userid)) { 406 | $this->redis->srem("user:device:$userid", $id); 407 | $this->redis->del("device:$id"); 408 | } 409 | } 410 | } 411 | 412 | // Clear devices with empty input processLists 413 | public function clean($userid,$active=0,$dryrun=0) { 414 | $userid = (int) $userid; 415 | $active = (int) $active; 416 | 417 | $now = time(); 418 | 419 | $deleted_inputs = 0; 420 | $deleted_nodes = 0; 421 | 422 | $result = $this->mysqli->query("SELECT `id`,`userid`,`nodeid`,`name`,`description`,`type`,`devicekey` FROM device WHERE userid = '$userid'"); 423 | while ($row = $result->fetch_object()) { 424 | 425 | $id = $row->id; 426 | $nodeid = $row->nodeid; 427 | 428 | // Fetch inputs associated with node 429 | $inputs = array(); 430 | if ($result2 = $this->mysqli->query("SELECT * FROM input WHERE `userid` = '$userid' AND `nodeid` = '$nodeid'")) { 431 | while ($row2 = $result2->fetch_object()) $inputs[] = $row2; 432 | } 433 | 434 | // Check that all node inputs are empty 435 | $inputs_empty = true; 436 | foreach ($inputs as $i) { 437 | $inputid = $i->id; 438 | 439 | if ($i->processList!=NULL && $i->processList!='') { 440 | $inputs_empty = false; 441 | } 442 | 443 | if ($active && $this->redis) { 444 | $input_time = $this->redis->hget("input:lastvalue:$inputid",'time'); 445 | if (($now-$input_time)<$active) { 446 | $inputs_empty = false; 447 | } 448 | } 449 | } 450 | 451 | if ($inputs_empty) { 452 | // Delete node 453 | if (!$dryrun) $this->delete($id); 454 | 455 | // Delete inputs 456 | foreach ($inputs as $i) { 457 | $inputid = $i->id; 458 | if (!$dryrun) { 459 | $this->mysqli->query("DELETE FROM input WHERE userid = '$userid' AND id = '$inputid'"); 460 | if ($this->redis) { 461 | $this->redis->del("input:$inputid"); 462 | $this->redis->srem("user:inputs:$userid",$inputid); 463 | } 464 | } 465 | $deleted_inputs++; 466 | } 467 | $deleted_nodes++; 468 | } 469 | } 470 | if ($dryrun) return "DRYRUN: $deleted_nodes nodes to delete ($deleted_inputs inputs)"; 471 | return "Deleted $deleted_nodes nodes ($deleted_inputs inputs)"; 472 | } 473 | 474 | public function set_fields($id, $fields) { 475 | $id = intval($id); 476 | if (!$this->exist($id)) { 477 | if (!$this->redis || !$this->load_device_to_redis($id)) { 478 | return array('success'=>false, 'message'=>'Device does not exist'); 479 | } 480 | } 481 | $success = true; 482 | 483 | $fields = json_decode(stripslashes($fields)); 484 | 485 | if (isset($fields->name)) { 486 | if (preg_replace('/[^\p{N}\p{L}_\s\-:]/u','',$fields->name)!=$fields->name) return array('success'=>false, 'message'=>'invalid characters in device name'); 487 | $stmt = $this->mysqli->prepare("UPDATE device SET name = ? WHERE id = ?"); 488 | $stmt->bind_param("si",$fields->name,$id); 489 | if ($stmt->execute()) { 490 | $this->redis->hSet("device:".$id,"name",$fields->name); 491 | } else $success = false; 492 | $stmt->close(); 493 | } 494 | 495 | if (isset($fields->description)) { 496 | if (preg_replace('/[^\p{N}\p{L}_\s\-:]/u','',$fields->description)!=$fields->description) return array('success'=>false, 'message'=>'invalid characters in device description'); 497 | $stmt = $this->mysqli->prepare("UPDATE device SET description = ? WHERE id = ?"); 498 | $stmt->bind_param("si",$fields->description,$id); 499 | if ($stmt->execute()) { 500 | $this->redis->hSet("device:".$id,"description",$fields->description); 501 | } else $success = false; 502 | $stmt->close(); 503 | } 504 | 505 | if (isset($fields->nodeid)) { 506 | if (preg_replace('/[^\p{N}\p{L}_\s\-:]/u','',$fields->nodeid)!=$fields->nodeid) return array('success'=>false, 'message'=>'invalid characters in device nodeid'); 507 | $stmt = $this->mysqli->prepare("UPDATE device SET nodeid = ? WHERE id = ?"); 508 | $stmt->bind_param("si",$fields->nodeid,$id); 509 | if ($stmt->execute()) { 510 | $this->redis->hSet("device:".$id,"nodeid",$fields->nodeid); 511 | } else $success = false; 512 | $stmt->close(); 513 | } 514 | 515 | if (isset($fields->type)) { 516 | if (preg_replace('/[^\/\|\,\w\s\-:]/','',$fields->type)!=$fields->type) return array('success'=>false, 'message'=>'invalid characters in device type'); 517 | $stmt = $this->mysqli->prepare("UPDATE device SET type = ? WHERE id = ?"); 518 | $stmt->bind_param("si",$fields->type,$id); 519 | if ($stmt->execute()) { 520 | $this->redis->hSet("device:".$id,"type",$fields->type); 521 | } else $success = false; 522 | $stmt->close(); 523 | } 524 | 525 | if (isset($fields->devicekey)) { 526 | // 1. Only allow alphanumeric characters 527 | if (!ctype_alnum($fields->devicekey)) return array('success'=>false, 'message'=>'invalid characters in device key'); 528 | 529 | // 2. Only allow 32 character length 530 | if (strlen($fields->devicekey)!=32) return array('success'=>false, 'message'=>'device key must be 32 characters long'); 531 | 532 | $stmt = $this->mysqli->prepare("UPDATE device SET devicekey = ? WHERE id = ?"); 533 | $stmt->bind_param("si",$fields->devicekey,$id); 534 | if ($stmt->execute()) { 535 | $this->redis->hSet("device:".$id,"devicekey",$fields->devicekey); 536 | } else $success = false; 537 | $stmt->close(); 538 | } 539 | 540 | if ($success) { 541 | return array('success'=>true, 'message'=>'Field updated'); 542 | } else { 543 | return array('success'=>false, 'message'=>'Field could not be updated'); 544 | } 545 | } 546 | 547 | public function set_new_devicekey($id) { 548 | $id = intval($id); 549 | if (!$this->exist($id)) { 550 | if (!$this->redis || !$this->load_device_to_redis($id)) { 551 | return array('success'=>false, 'message'=>'Device does not exist'); 552 | } 553 | } 554 | 555 | $devicekey = md5(uniqid(mt_rand(), true)); 556 | 557 | $stmt = $this->mysqli->prepare("UPDATE device SET devicekey = ? WHERE id = ?"); 558 | $stmt->bind_param("si",$devicekey,$id); 559 | $result = $stmt->execute(); 560 | $stmt->close(); 561 | 562 | if ($result) { 563 | $this->redis->hSet("device:".$id,"devicekey",$devicekey); 564 | return $devicekey; 565 | } else { 566 | return false; 567 | } 568 | } 569 | 570 | public function get_template_list() { 571 | return $this->load_template_list(); 572 | } 573 | 574 | public function get_template_list_meta() { 575 | $templates = array(); 576 | 577 | if ($this->redis) { 578 | if (!$this->redis->exists("device:templates:meta")) $this->load_template_list(); 579 | 580 | $ids = $this->redis->sMembers("device:templates:meta"); 581 | foreach ($ids as $id) { 582 | $template = $this->redis->hGetAll("device:template:$id"); 583 | $template["control"] = (bool) $template["control"]; 584 | 585 | $templates[$id] = $template; 586 | } 587 | } 588 | else { 589 | if (empty($this->templates)) { // Cache it now 590 | $this->load_template_list(); 591 | } 592 | $templates = $this->templates; 593 | } 594 | ksort($templates); 595 | return $templates; 596 | } 597 | 598 | private function get_template_meta($id) { 599 | if ($this->redis) { 600 | if ($this->redis->exists("device:template:$id")) { 601 | $template = $this->redis->hGetAll("device:template:$id"); 602 | $template["control"] = (bool) $template["control"]; 603 | 604 | return $template; 605 | } 606 | } 607 | else { 608 | if (empty($this->templates)) { // Cache it now 609 | $this->load_template_list(); 610 | } 611 | if(isset($this->templates[$id])) { 612 | return $this->templates[$id]; 613 | } 614 | } 615 | return array('success'=>false, 'message'=>'Device template does not exist'); 616 | } 617 | 618 | public function get_template($id) { 619 | 620 | $result = $this->get_template_meta($id); 621 | if (isset($result['success']) && $result['success'] == false) { 622 | return $result; 623 | } 624 | $module = $result['module']; 625 | $class = $this->get_module_class($module, self::TEMPLATE); 626 | if ($class != null) { 627 | return $class->get_template($id); 628 | } 629 | return array('success'=>false, 'message'=>'Device template class is not defined'); 630 | } 631 | 632 | public function prepare_template($id) { 633 | $id = intval($id); 634 | 635 | $device = $this->get($id); 636 | if (isset($device['type']) && $device['type'] != 'null' && $device['type']) { 637 | $result = $this->get_template_meta($device['type']); 638 | if (isset($result["success"]) && $result["success"] == false) { 639 | return $result; 640 | } 641 | $module = $result['module']; 642 | $class = $this->get_module_class($module, self::TEMPLATE); 643 | if ($class != null) { 644 | return $class->prepare_template($device); 645 | } 646 | return array('success'=>false, 'message'=>'Device template class is not defined'); 647 | } 648 | return array('success'=>false, 'message'=>'Device type not specified'); 649 | } 650 | 651 | public function init($id, $template) { 652 | $id = intval($id); 653 | 654 | $device = $this->get($id); 655 | $result = $this->init_template($device, $template); 656 | if (isset($result['success']) && $result['success'] == false) { 657 | return $result; 658 | } 659 | return array('success'=>true, 'message'=>'Device initialized'); 660 | } 661 | 662 | public function init_template($device, $template) { 663 | if (isset($template) && $template!==false) $template = json_decode($template); 664 | 665 | if (isset($device['type']) && $device['type'] != 'null' && $device['type']) { 666 | $result = $this->get_template_meta($device['type']); 667 | if (isset($result['success']) && $result['success'] == false) { 668 | return $result; 669 | } 670 | $module = $result['module']; 671 | $class = $this->get_module_class($module, self::TEMPLATE); 672 | if ($class != null) { 673 | return $class->init_template($device, $template); 674 | } 675 | return array('success'=>false, 'message'=>'Device template class is not defined'); 676 | } 677 | return array('success'=>false, 'message'=>'Device type not specified'); 678 | } 679 | 680 | public function reload_template_list() { 681 | $result = $this->load_template_list(); 682 | if (isset($result['success']) && $result['success'] == false) { 683 | return $result; 684 | } 685 | return array('success'=>true, 'message'=>'Templates successfully reloaded'); 686 | } 687 | 688 | private function load_template_list() { 689 | 690 | if ($this->redis) { 691 | foreach ($this->redis->sMembers("device:templates:meta") as $id) { 692 | $this->redis->del("device:template:$id"); 693 | } 694 | $this->redis->del("device:templates:meta"); 695 | } 696 | else { 697 | $this->templates = array(); 698 | } 699 | $templates = array(); 700 | 701 | $dir = scandir("Modules"); 702 | for ($i=2; $iget_module_class($dir[$i], self::TEMPLATE); 705 | if ($class != null) { 706 | $result = $class->get_template_list(); 707 | if (isset($result['success']) && $result['success'] == false) { 708 | return $result; 709 | } 710 | foreach($result as $key => $value) { 711 | $this->cache_template($dir[$i], $key, $value); 712 | $templates[$key] = $value; 713 | } 714 | } 715 | } 716 | } 717 | 718 | return $templates; 719 | } 720 | 721 | private function cache_template($module, $id, $template) { 722 | $meta = array( 723 | "module"=>$module 724 | ); 725 | $meta["name"] = ((!isset($template->name) || $template->name == "" ) ? $id : $template->name); 726 | $meta["category"] = ((!isset($template->category) || $template->category== "" ) ? "General" : $template->category); 727 | $meta["group"] = ((!isset($template->group) || $template->group== "" ) ? "Miscellaneous" : $template->group); 728 | $meta["description"] = (!isset($template->description) ? "" : $template->description); 729 | $meta["control"] = (!isset($template->control) ? false : true); 730 | 731 | if ($this->redis) { 732 | $this->redis->sAdd("device:templates:meta", $id); 733 | $this->redis->hMSet("device:template:$id", $meta); 734 | } 735 | else { 736 | $this->templates[$id] = $meta; 737 | } 738 | } 739 | 740 | private function get_module_class($module, $type) { 741 | /* 742 | magic function __call (above) MUST BE USED with this. 743 | Load additional template module files. 744 | Looks in the folder Modules/modulename/ for a file modulename_template.php 745 | (module_name all lowercase but class ModulenameTemplate in php file that is CamelCase) 746 | */ 747 | $module_file = "Modules/".$module."/".$module."_".$type.".php"; 748 | $module_class = null; 749 | if(file_exists($module_file)){ 750 | require_once($module_file); 751 | 752 | $module_class_name = ucfirst(strtolower($module)).ucfirst($type); 753 | $module_class = new $module_class_name($this); 754 | } 755 | return $module_class; 756 | } 757 | } 758 | --------------------------------------------------------------------------------