├── .gitignore ├── Documentation ├── .DS_Store └── openLuup User Guide.pdf ├── LICENSE ├── README.md ├── Utilities ├── .DS_Store ├── openLuup_install.lua ├── openLuup_reload └── openLuup_reload.bat ├── VeraBridge └── VeraBridge.png ├── cgi-bin ├── .DS_Store └── cmh │ ├── .DS_Store │ └── upload_upnp_file.sh ├── cgi ├── hello.lua └── whisper-edit.lua ├── icons ├── AltAppStore.png ├── AltAppStore.svg ├── GitHub-Mark-32px.png ├── GitHub-Mark-64px.png ├── VeraBridge.png ├── VeraBridge.svg └── openLuup.svg ├── openLuup ├── L_AltAppStore.lua ├── L_ShellyBridge.lua ├── L_TasmotaBridge.lua ├── L_VeraBridge.lua ├── L_Zigbee2MQTTBridge.lua ├── L_openLuup.lua ├── api.lua ├── backup.lua ├── chdev.lua ├── client.lua ├── compression.lua ├── console.lua ├── console_util.lua ├── devices.lua ├── gateway.lua ├── graphite_cgi.lua ├── historian.lua ├── http_async.lua ├── init.lua ├── io.lua ├── json.lua ├── loader.lua ├── logs.lua ├── luup.lua ├── mqtt.lua ├── mqtt_util.lua ├── panels.lua ├── pop3.lua ├── requests.lua ├── scenes.lua ├── scheduler.lua ├── server.lua ├── servertables.lua ├── servlet.lua ├── smtp.lua ├── sysinfo.lua ├── tcp.lua ├── timers.lua ├── userdata.lua ├── virtualfilesystem.lua ├── whisper-edit.lua ├── whisper.lua ├── wsapi.lua └── xml.lua └── tests ├── .DS_Store ├── data └── .DS_Store ├── luaunit.lua ├── test_all.lua ├── test_chdev.lua ├── test_compression.lua ├── test_devices.lua ├── test_gateway.lua ├── test_hag.lua ├── test_io.lua ├── test_json.lua ├── test_loader.lua ├── test_logs.lua ├── test_luup.lua ├── test_requests.lua ├── test_rooms.lua ├── test_scenes.lua ├── test_scheduler.lua ├── test_server.lua ├── test_template.lua ├── test_timers.lua ├── test_userdata.lua ├── test_vfs.lua ├── test_whisper.lua ├── test_xml.lua └── test_xml_html.lua /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | openLuup/.DS_Store 5 | -------------------------------------------------------------------------------- /Documentation/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/Documentation/.DS_Store -------------------------------------------------------------------------------- /Documentation/openLuup User Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/Documentation/openLuup User Guide.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openLuup 2 | a pure-Lua open-source emulation of the Vera Luup environment 3 | 4 | **openLuup** is an environment which supports the running of some MiOS (Vera) plugins on generic Unix systems (or, indeed, Windows systems.) Processors such as Raspberry Pi and BeagleBone Black are ideal for running this environment, although it can also run on Apple Mac, Microsoft Windows PCs, anything, in fact, which can run Lua code (most things can - even an Arduino Yún board.) The intention is to offload processing (cpu and memory use) from a running Vera to a remote machine to increase system reliability. 5 | 6 | Running on non-specific hardware means that there is no native support for Z-wave, although plugins to handle Z-wave USB sticks can support this. The full range of MySensors (http://www.mysensors.org/) Arduino devices are supported though the Ethernet Bridge plugin available on that site. A plugin to provide a bi-directional ‘bridge’ (monitoring / control) to remote MiOS (Vera) systems is provided in the openLuup installation. 7 | 8 | **openLuup** is extremely fast to start (a few seconds before it starts running any created devices startup code) has very low cpu load, and has a very compact memory footprint. Whereas each plugin on a Vera system might take ~4 Mbytes, it’s far less than this under openLuup, in fact, the whole system can fit into that sort of space. Since the hardware on which it runs is anyway likely to have much more physical memory than current Vera systems, memory is not really an issue. 9 | 10 | There is no built-in user interface, but we have, courtesy of @amg0, the most excellent altUI: Alternate UI to UI7 (see the Vera forum board http://forum.micasaverde.com/index.php/board,78.0.html) An automated way of installing and updating the ALTUI environment is now built-in to openLuup. There’s actually no requirement for any user interface if all that’s needed is an environment to run plugins. 11 | 12 | Devices, scenes, rooms and attributes are persisted across restarts. The startup initialisation process supports both the option of starting with a ‘factory-reset’ system, or any saved image, or continuing seamlessly with the previously saved environment. A built-in utility is provided to transfer a complete set of uncompressed device files and icons from any Vera on your network to the openLuup target machine. 13 | 14 | What **openLuup** does: 15 | 16 | * runs the ALTUI plugin to give a great UI experience 17 | * runs the MySensors Arduino plugin (ethernet connection to gateway only) which is really the main goal - to have a Vera-like machine built entirely from third-party bits (open source) 18 | * includes a bridge app to link to remote Veras (which can be running UI5 or UI7 and require no additional software.) 19 | * runs many plugins unmodified – particularly those which just create virtual devices 
 (eg. Netatmo, ...) 20 | * uses a tiny amount of memory and boots up very quickly (a few seconds) 21 | * supports scenes with timers and ALTUI-style triggers 22 | * has its own port 3480 HTTP server supporting multiple asynchronous client requests 23 | * has a fairly complete implementation of the Luup API and the HTTP requests 24 | * has a simple to understand log structure. 25 | * writes variables to a separate log file for ALTUI to display variable and scene changes. 26 | 27 | 28 | What it doesn't do: 29 | 30 | * Some less-used HTML requests are not yet implemented. 31 | * Doesn't support the incoming or timeout action tags in service files, 
 but does support the device-level incoming tag (for asynchronous socket I/O.) 32 | * Doesn’t directly support local serial I/O hardware (there are work-arounds.) 33 | * Doesn't run encrypted, or licensed, plugins. 34 | * Doesn't use lots of memory. 35 | * Doesn’t use lots of cpu. 36 | * Doesn’t constantly reload (like Vera often does, for no apparent reason.) 37 | * Doesn't do UPnP (and never will.) 
 38 | -------------------------------------------------------------------------------- /Utilities/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/Utilities/.DS_Store -------------------------------------------------------------------------------- /Utilities/openLuup_install.lua: -------------------------------------------------------------------------------- 1 | -- first-time download and install of openLuup files from GitHub 2 | 3 | -- 2018.02.17 add local ./www/ directory 4 | -- 2018.08.05 use servertables for myIP (thanks @samunders) 5 | -- 2018.08.10 add parameter for install branch (thanks @vwout) 6 | 7 | -- 2019.02.15 fix tslv1_2 protocol error (properly, this time!) 8 | 9 | -- 2024.10.28 fix moved dkjson url (thanks @nodecentral) 10 | 11 | 12 | local lua = "lua5.1" -- change this to "lua" if required 13 | 14 | local x = os.execute 15 | local p = print 16 | local branch = arg[1] or "master" 17 | 18 | p "openLuup_install 2019.02.15 @akbooer" 19 | 20 | local http = require "socket.http" 21 | local https = require "ssl.https" 22 | local ltn12 = require "ltn12" 23 | local lfs = require "lfs" 24 | 25 | p ("getting openLuup version tar file from GitHub branch " .. branch .. "...") 26 | 27 | local _, code = https.request{ 28 | url = "https://codeload.github.com/akbooer/openLuup/tar.gz/" .. branch, 29 | sink = ltn12.sink.file(io.open("latest.tar.gz", "wb")), 30 | protocol = "tlsv1_2", 31 | } 32 | 33 | assert (code == 200, "GitHub download failed with code " .. code) 34 | 35 | p "un-zipping download files..." 36 | 37 | x "tar -xf latest.tar.gz" 38 | x ("mv openLuup-" .. branch .. "/openLuup/ .") 39 | x ("rm -r openLuup-" .. branch .. "/") 40 | 41 | p "getting dkjson.lua..." 42 | _, code = http.request{ 43 | url = "http://dkolf.de/dkjson-lua/dkjson-2.8.lua", 44 | sink = ltn12.sink.file(io.open("dkjson.lua", "wb")), 45 | } 46 | 47 | assert (code == 200, "GitHub download failed with code " .. code) 48 | 49 | p "creating required files and folders" 50 | lfs.mkdir "www" 51 | lfs.mkdir "files" 52 | lfs.mkdir "icons" 53 | lfs.mkdir "backup" -- thanks @a-lurker 54 | 55 | local vfs = require "openLuup.virtualfilesystem" 56 | 57 | local function add_vfs_file (name) 58 | local f = io.open (name, "wb") 59 | f: write (vfs.read (name)) 60 | f: close () 61 | end 62 | 63 | add_vfs_file "index.html" 64 | 65 | local reload = "openLuup_reload" 66 | local pathSeparator = package.config:sub(1,1) -- thanks to @vosmont for this Windows/Unix discriminator 67 | if pathSeparator ~= '/' then reload = reload .. ".bat" end -- Windows version 68 | 69 | add_vfs_file (reload) 70 | 71 | p "initialising..." 72 | 73 | x "chmod a+x openLuup_reload" 74 | 75 | local s= require "openLuup.servertables" 76 | local ip = s.myIP or "openLuupIP" 77 | 78 | p "downloading and installing AltUI..." 79 | x (lua .. " openLuup/init.lua altui") 80 | 81 | x "./openLuup_reload &" 82 | p "openLuup downloaded, installed, and running..." 83 | p ("visit http://" .. ip .. ":3480 to start using the system") 84 | 85 | ----- 86 | -------------------------------------------------------------------------------- /Utilities/openLuup_reload: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # reload loop for openLuup 4 | # @akbooer, Aug 2015 5 | # you may need to change ‘lua’ to ‘lua5.1’ depending on your install 6 | 7 | lua5.1 openLuup/init.lua $1 8 | 9 | while [ $? -eq 42 ] 10 | do 11 | lua5.1 openLuup/init.lua 12 | done 13 | -------------------------------------------------------------------------------- /Utilities/openLuup_reload.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | SETLOCAL 3 | SET LUA_DEV=D:\devhome\app\LuaDist\bin 4 | SET CURRENT_PATH=%~dp0 5 | ECHO Start openLuup from "%CURRENT_PATH%" 6 | ECHO. 7 | CD %CURRENT_PATH% 8 | "%LUA_DEV%\lua" openLuup\init.lua %1 9 | 10 | :loop 11 | IF NOT %ERRORLEVEL% == 42 GOTO exit 12 | "%LUA_DEV%\lua" openLuup\init.lua 13 | GOTO loop 14 | 15 | :exit 16 | -------------------------------------------------------------------------------- /VeraBridge/VeraBridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/VeraBridge/VeraBridge.png -------------------------------------------------------------------------------- /cgi-bin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/cgi-bin/.DS_Store -------------------------------------------------------------------------------- /cgi-bin/cmh/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/cgi-bin/cmh/.DS_Store -------------------------------------------------------------------------------- /cgi-bin/cmh/upload_upnp_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | -- @vosmont's implementation of /cgi-bin/cmh/upload_upnp_file.sh 6 | -- converted to WSAPI application by @akbooer 7 | -- see: http://keplerproject.github.io/wsapi/manual.html 8 | -- 9 | -- 2016.02.18 extract vosmont's modification from openLuup.server and make into WSAPI app 10 | -- 11 | 12 | local _log -- defined from WSAPI environment as error.write(...) in run() method. 13 | 14 | -- create the file 15 | local function upload_file (URL) 16 | local file, status, result 17 | local path = "" -- should use a specific folder for security reason ? 18 | local fileName, fileContent = URL.upnp_file_1_name, URL.upnp_file_1 19 | if fileName and fileContent then 20 | file = io.open(path .. fileName, "w") 21 | end 22 | fileName = fileName or '?' 23 | if (file == nil) then 24 | _log ("File '" .. path .. fileName .. "' cannot be created") 25 | result = "KO" 26 | status = 400 -- Bad Request. 27 | else 28 | file:write(fileContent) 29 | file:close() 30 | _log ("File '" .. path .. fileName .. "' has been written") 31 | result = "OK" 32 | status = 201 -- Created. The request has been fulfilled and resulted in a new resource being created 33 | end 34 | return status, result .. "|" .. fileName, "text/html" 35 | end 36 | 37 | -- gets headers in multipart content 38 | local function read_part_headers (content, pos) 39 | local EOH = "\r\n\r\n" 40 | local i, j = string.find(content, EOH, pos, true) 41 | if i then 42 | local header_data = string.sub(content, pos, j - 1) 43 | local headers = {} 44 | for type, val in string.gmatch(header_data, '([^%c%s:]+):%s+([^\n]+)') do 45 | headers[type] = val 46 | end 47 | return headers, j + 1 48 | else 49 | return nil, pos 50 | end 51 | end 52 | 53 | -- gets fields in multipart headers 54 | local function get_field_names(headers) 55 | local disp_header = headers["Content-Disposition"] or "" 56 | local attrs = {} 57 | for attr, val in string.gmatch(disp_header, ';%s*([^%s=]+)="(.-)"') do 58 | attrs[attr] = val 59 | end 60 | return attrs.name, attrs.filename and string.match(attrs.filename, "[/\\]?([^/\\]+)$") 61 | end 62 | 63 | -- gets the data in multipart content 64 | local function read_field_content(content, boundary, pos) 65 | local i, j = string.find(content, "\r\n" .. boundary, pos, true) 66 | if i then 67 | return string.sub(content, pos, i - 1), j + 1 68 | else 69 | return nil, pos 70 | end 71 | end 72 | 73 | 74 | -- global entry point called by WSAPI connector 75 | 76 | --[[ 77 | 78 | The environment is a Lua table containing the CGI metavariables (at minimum the RFC3875 ones) plus any 79 | server-specific metainformation. It also contains an input field, a stream for the request's data, 80 | and an error field, a stream for the server's error log. 81 | 82 | The input field answers to the read([n]) method, 83 | where n is the number of bytes you want to read 84 | (or nil if you want the whole input). 85 | 86 | The error field answers to the write(...) method. 87 | 88 | return values: the HTTP status code, a table with headers, and the output iterator. 89 | 90 | --]] 91 | 92 | function run (wsapi_env) 93 | _log = wsapi_env.error.write -- set up the log output 94 | 95 | -- vosmont : add upload file management 96 | -- inspired from https://github.com/keplerproject/wsapi 97 | local URL = {} 98 | 99 | -- get POST content, 100 | local post_content = wsapi_env.input.read() 101 | 102 | local content_type = wsapi_env["CONTENT_TYPE"] 103 | 104 | -- get uploaded file 105 | if string.find(content_type, "multipart/form-data", 1, true) then 106 | local boundary = "--" .. string.match(content_type, "boundary%=(.-)$") 107 | -- get all the parts 108 | local pos = 1 109 | local _, part_headers, name, value 110 | _, pos = string.find(post_content, boundary, 1, true) 111 | pos = pos + 1 112 | part_headers, pos = read_part_headers(post_content, pos) 113 | while (part_headers) do 114 | --_log ("HTTP POST request multipart headers : " .. json.encode(part_headers)) 115 | name, _ = get_field_names(part_headers) -- do not use "file_name" in Vera implementation of uploading files 116 | value, pos = read_field_content(post_content, boundary, pos) 117 | URL[name] = value 118 | -- prepare next multipart scan 119 | part_headers, pos = read_part_headers(post_content, pos) 120 | end 121 | end 122 | --_log ("HTTP POST request content : " .. post_content) 123 | --_log ("HTTP POST request content : " .. json.encode(URL)) 124 | 125 | local status, return_content, return_content_type = upload_file (URL) 126 | 127 | local headers = {["Content-Type"] = return_content_type} 128 | 129 | local function iterator () -- one-shot iterator, returns content, then nil 130 | local x = return_content 131 | return_content = nil 132 | return x 133 | end 134 | 135 | return status, headers, iterator 136 | end 137 | 138 | ----- 139 | -------------------------------------------------------------------------------- /cgi/hello.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | function run(wsapi_env) 6 | local headers = { ["Content-type"] = "text/html" } 7 | 8 | local function hello_text() 9 | coroutine.yield("") 10 | coroutine.yield("

Hello Wsapi!

") 11 | coroutine.yield("

PATH_INFO: " .. wsapi_env.PATH_INFO .. "

") 12 | coroutine.yield("

SCRIPT_NAME: " .. wsapi_env.SCRIPT_NAME .. "

") 13 | coroutine.yield("") 14 | end 15 | 16 | return 200, headers, coroutine.wrap(hello_text) 17 | end 18 | -------------------------------------------------------------------------------- /cgi/whisper-edit.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | ABOUT = { 6 | NAME = "whisper-edit", 7 | VERSION = "2019.07.15", 8 | DESCRIPTION = "Whisper database editor script cgi/whisper-edit.lua", 9 | AUTHOR = "@akbooer", 10 | COPYRIGHT = "(c) 2013-2019 AKBooer", 11 | DOCUMENTATION = "", 12 | } 13 | 14 | -- Whisper file editor, using storage finder and WSAPI request and response libraries 15 | 16 | -- 2016.07.06 based on original whisper-editor 17 | -- 2019.06.07 use w3.css style sheets 18 | -- 2019.06.29 use xhtml module 19 | -- 2019.07.15 use new xml.createHTMLDocument() factory method 20 | 21 | 22 | local whisper = require "openLuup.whisper" 23 | local wsapi = require "openLuup.wsapi" -- for request library 24 | local xml = require "openLuup.xml" 25 | 26 | 27 | local _log -- defined from WSAPI environment as wsapi.error:write(...) in run() method. 28 | 29 | -- global entry point called by WSAPI connector 30 | 31 | 32 | local pagename = "w-edit" 33 | 34 | ----------------------------------- 35 | 36 | local button_class = "w3-button w3-border w3-margin w3-round-large " 37 | 38 | local function ymd (date, hour, min,sec) 39 | local y,m,d = (date or ''): match "(%d%d%d%d)%D(%d%d)%D(%d%d)" 40 | if y then 41 | return os.time {year=y, month=m, day=d, hour=hour, min=min, sec=sec} 42 | end 43 | end 44 | 45 | 46 | ----------------------------------- 47 | -- for future use...? 48 | --[[ 49 | 50 | -- find min and max times in tv array (interleaved times and values) 51 | -- note that a time of zero means, in fact, undefined 52 | local function min_max (x) 53 | local min,max = os.time(),x[1] 54 | for i = 1,#x, 2 do 55 | local t = x[i] 56 | if t > 0 then 57 | if t > max then max = t end 58 | if t < min then min = t end 59 | end 60 | end 61 | return min, max 62 | end 63 | 64 | -- gets the timestamp of the oldest and newest datapoints in file 65 | local function earliest_latest (header) 66 | local archives = header.archives 67 | -- search for latest in youngest archive 68 | local youngest = archives[1].readall() 69 | local _, late = min_max (youngest) 70 | -- early = os.time() - header['maxRetention'] -- instead, search for earliest in oldest archive 71 | local oldest = archives[#archives].readall() 72 | local early = min_max (oldest) 73 | if late < early then late = early end 74 | return Interval (early, late) 75 | end 76 | --]] 77 | 78 | ----------------------------------- 79 | 80 | function run (wsapi_env) 81 | 82 | _log = function (...) wsapi_env.error:write(...) end -- set up the log output, note colon syntax 83 | 84 | local req = wsapi.request.new (wsapi_env) -- use request library to get object with useful methods 85 | local res = wsapi.response.new () -- and the response library to build the response! 86 | 87 | 88 | -- read the basic parameters from ETHER the GET or the POST parameters 89 | 90 | local now = os.time() 91 | local date = "%Y-%m-%d" 92 | local params = req.params 93 | local target = params["target"] 94 | local from = params["from"] 95 | local to = params["until"] 96 | from = ymd(from) and from or os.date (date, now) 97 | to = ymd(to) and to or os.date (date, now) 98 | 99 | -- get the requested data 100 | 101 | local I, V, T 102 | local tv = whisper.fetch (target, ymd(from, 0,0,0), ymd(to, 23,59,59)) 103 | if tv then 104 | local n = 0 105 | I, V, T = {}, {}, {} -- I is index table 106 | for _, v,t in tv:ipairs () do 107 | if v then -- only show non-nil data 108 | n = n + 1 109 | T[n] = t 110 | V[n] = v 111 | I[tostring(t)] = n -- also index by text time (since post requests come that way) 112 | end 113 | end 114 | end 115 | 116 | -- POST processing: if valid data and updates, then make changes 117 | 118 | if V and req.method == "POST" then 119 | local post = req.POST 120 | local Tedit, Vedit = {}, {} 121 | for t,v in pairs (post) do -- NB: t is a string 122 | local tn = tonumber (t) 123 | local vn = tonumber (v) 124 | local idx = I[t] 125 | if tn and vn and V[idx] ~= vn then -- has been edited 126 | -- print (tn, "old: " .. V[idx], "new: " .. vn) 127 | Tedit[#Tedit+1] = tn 128 | Vedit[#Vedit+1] = vn 129 | V[idx] = vn -- update table with new value 130 | end 131 | end 132 | 133 | whisper.setAggregationMethod (target, post.aggregation, post.xFilesFactor) -- update aggregation 134 | 135 | luup.log ("Graphite Editor - Number of edits: " .. #Vedit) 136 | -- whisper.update_many (path,values,timestamps, now) 137 | local ok = pcall (whisper.update_many, target, Vedit, Tedit, now) 138 | if not ok then luup.log ("Whisper file update failed: " .. target) end 139 | end 140 | 141 | -- 142 | -- build the HTML page 143 | -- 144 | 145 | local h = xml.createHTMLDocument "W-Edit" 146 | 147 | local read_form = h.div {class = "w3-card w3-margin w3-small", 148 | h.div {class = "w3-container w3-grey", 149 | h.h4 {"Database Query"}}, 150 | h.form {class = "w3-container w3-margin-top", 151 | action=req.script_name, 152 | method="get", 153 | h.input {type="hidden", name="page", value=pagename}, 154 | h.label {"target: ", title="full file path"}, 155 | h.input {class = "w3-input", type="text", name="target", value=target}, 156 | h.label {"from: ", title="from start of this day"}, 157 | h.input {class = "w3-input", type="date", name="from", value=from}, 158 | h.label {"until: ", title="until end of this day"}, 159 | h.input {class = "w3-input", type="date", name="until", value=to}, 160 | h.div {class = "w3-right-align", 161 | h.input {class = button_class .. "w3-pale-green", 162 | type="Submit", value="Read", title="get data to edit"} 163 | }, 164 | }, 165 | } 166 | 167 | -- if there is any data, then build an editable table 168 | 169 | local function row (time, value) 170 | return {os.date ("%Y-%m-%d %H:%M:%S", time), h.input {name=time, value=value}} 171 | end 172 | 173 | local data = '' -- default to blank space 174 | if V then 175 | data = h.table {class = "w3-table"} 176 | data: header {"date / time", "value"} 177 | for i,v in ipairs (V) do 178 | data: row (row (T[i], v)) 179 | end 180 | end 181 | 182 | local info = whisper.info (target) 183 | local aggregation = {} 184 | for _, method in ipairs (whisper.aggregationTypeToMethod) do 185 | local checked 186 | if method == info.aggregationMethod then checked = '1' end 187 | aggregation[#aggregation+1] = h.label {method} 188 | aggregation[#aggregation+1] = h.input {type="radio", name="aggregation", value=method, checked=checked} 189 | end 190 | 191 | local xff = ("%0.2f"):format (info.xFilesFactor) 192 | 193 | local write_form = h.div {class = "w3-card w3-margin w3-small", 194 | h.div {class = "w3-container w3-grey", 195 | h.h4 {"Database Update"}}, 196 | h.form {class = "w3-container w3-margin-top", 197 | action=req.script_name, 198 | method="post", 199 | h.input {type="hidden", name = "page", value = pagename}, 200 | h.input {type="hidden", name = "from", value = from}, 201 | h.input {type="hidden", name = "until", value = to}, 202 | h.input {type="hidden", name = "target", value = target}, 203 | h.label {"archives: ",title="sample rate:time span, ..., for each resolution archive"}, 204 | h.input {class = "w3-input", readonly=1, disabled=1, value = tostring(info.retentions)}, 205 | h.label {"aggregation:", title="function for combining samples between archives"}, 206 | h.div {class = "w3-white w3-padding w3-border-bottom", h.div (aggregation) }, 207 | h.label {"xFilesFactor:", title = "xff (0-1) if you don't know what this is, don't change it"}, 208 | h.input {class = "w3-input", name="xFilesFactor", autocomplete="off", value = xff}, 209 | h.div {class = "w3-right-align", 210 | h.input {class = button_class .. "w3-pale-yellow", 211 | type="Reset", title="clear changes to table"}, 212 | h.input {class = button_class .. "w3-pale-red", 213 | type="Submit", value="Commit", title="write changes back to file"}, 214 | }, 215 | h.div {class = "w3-panel w3-border w3-hover-border-red", data}, 216 | }} 217 | 218 | h.body:appendChild { 219 | h.meta {charset="utf-8", name="viewport", content="width=device-width, initial-scale=1"}, 220 | h.link {rel="stylesheet", href="https://www.w3schools.com/w3css/4/w3.css"}, 221 | h.div {class = "w3-panel w3-cell", read_form, write_form}} 222 | 223 | res:write (tostring(h)) 224 | 225 | return res:finish() 226 | end 227 | 228 | ----- 229 | -------------------------------------------------------------------------------- /icons/AltAppStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/icons/AltAppStore.png -------------------------------------------------------------------------------- /icons/AltAppStore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /icons/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/icons/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /icons/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/icons/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /icons/VeraBridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/icons/VeraBridge.png -------------------------------------------------------------------------------- /icons/VeraBridge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/openLuup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /openLuup/L_TasmotaBridge.lua: -------------------------------------------------------------------------------- 1 | module(..., package.seeall) 2 | 3 | ABOUT = { 4 | NAME = "mqtt_tasmota", 5 | VERSION = "2021.06.14", 6 | DESCRIPTION = "Tasmota MQTT bridge", 7 | AUTHOR = "@akbooer", 8 | COPYRIGHT = "(c) 2020-2021 AKBooer", 9 | DOCUMENTATION = "", 10 | LICENSE = [[ 11 | Copyright 2013-2021 AK Booer 12 | 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | ]] 25 | } 26 | 27 | 28 | -- 2021.04.02 new L_TasmotaBridge file 29 | -- 2021.04.17 use openLuup device variable virtualizer, go one level deeper in table data (thanks @Buxton) 30 | -- 2021.05.08 add /STATE and /RESULT topics (thanks @ArcherS) 31 | -- 2021.05.10 add missing bridge_utilities.SID, "Remote_ID" 32 | -- 2021.06.08 LWT as text message (thanks @Buxton - fixes bridged broker dropout) 33 | -- see: https://smarthome.community/topic/506/openluup-tasmota-mqtt-bridge/115 34 | -- 2021.06.12 add startup(config) with user-definable Prefix and Topic lists (thanks @Buxton) 35 | 36 | 37 | local json = require "openLuup.json" 38 | local luup = require "openLuup.luup" 39 | local chdev = require "openLuup.chdev" -- to create new bridge devices 40 | local tables = require "openLuup.servertables" -- for standard DEV and SID definitions 41 | 42 | local DEV = tables.DEV { 43 | tasmota = "D_GenericTasmotaDevice.xml", 44 | } 45 | 46 | local SID = tables.SID { 47 | TasmotaBridge = "urn:akbooer-com:serviceId:TasmotaBridge1", 48 | } 49 | 50 | local openLuup = luup.openLuup 51 | local VIRTUAL = require "openLuup.api" 52 | 53 | local VALID = {} -- valid topics (set at startup) 54 | 55 | -------------------------------------------------- 56 | -- 57 | -- Tasmota MQTT Bridge - CONTROL 58 | -- 59 | -- this part runs as a standard device 60 | -- it is a control API only (ie. action requests) 61 | -- 62 | 63 | function init () -- Tasmota Bridge device entry point 64 | luup.set_failure (0) 65 | return true, "OK", "TasmotaBridge" 66 | end 67 | 68 | -- 69 | -- end of Luup device file 70 | -- 71 | -------------------------------------------------- 72 | 73 | 74 | 75 | -------------------------------------------------- 76 | -- 77 | -- Tasmota MQTT Bridge - MODEL and VIEW 78 | -- 79 | -- this part runs as a system module and can create a bridge device 80 | -- as well as subsequent child devices, and update their variables 81 | -- 82 | 83 | local devices = {} -- gets filled with device info on MQTT connection 84 | local devNo -- bridge device number (set on startup) 85 | 86 | 87 | local function _log (msg) 88 | luup.log (msg, "luup.tasmota") 89 | end 90 | 91 | local function create_device(altid) 92 | _log ("New Tasmota detected: " .. altid) 93 | local room = luup.rooms.create "Tasmota" -- create new device in Tasmota room 94 | 95 | local offset = VIRTUAL[devNo][SID.TasmotaBridge].Offset 96 | local dno = openLuup.bridge.nextIdInBlock(offset, 0) -- assign next device number in block 97 | 98 | local name = altid 99 | 100 | 101 | -- local upnp_file = models[info.model].upnp 102 | local upnp_file = DEV.tasmota 103 | 104 | local dev = chdev.create { 105 | devNo = dno, 106 | internal_id = altid, 107 | description = name, 108 | upnp_file = upnp_file, 109 | -- json_file = json_file, 110 | parent = devNo, 111 | room = room, 112 | manufacturer = "could be anyone", 113 | } 114 | 115 | dev.handle_children = true -- ensure that any child devices are handled 116 | luup.devices[dno] = dev -- add to Luup devices 117 | 118 | return dno 119 | end 120 | 121 | local function init_device (altid) 122 | local dno = openLuup.find_device {altid = altid} 123 | or 124 | create_device (altid) 125 | 126 | luup.devices[dno].handle_children = true -- ensure that it handles child requests 127 | devices[altid] = dno -- save the device number, indexed by id 128 | return dno 129 | end 130 | 131 | -- the bridge is a standard Luup plugin 132 | local function create_TasmotaBridge() 133 | local internal_id, ip, mac, hidden, invisible, parent, room, pluginnum 134 | 135 | local offset = openLuup.bridge.nextIdBlock() 136 | local statevariables = table.concat {SID.TasmotaBridge, ",Offset=", offset} 137 | 138 | return luup.create_device ( 139 | "TasmotaBridge", -- device_type 140 | internal_id, 141 | "Tasmota", -- description 142 | "D_TasmotaBridge.xml", -- upnp_file 143 | "I_TasmotaBridge.xml", -- upnp_impl 144 | 145 | ip, mac, hidden, invisible, parent, room, pluginnum, 146 | 147 | statevariables) 148 | end 149 | 150 | ----- 151 | -- 152 | -- MQTT callbacks 153 | -- 154 | 155 | function _G.Tasmota_MQTT_Handler (topic, message, prefix) 156 | 157 | local tasmotas = topic: match (table.concat {"^", prefix, "/(.+)"}) 158 | if not tasmotas then return end 159 | 160 | devNo = devNo -- ensure that TasmotaBridge device exists 161 | or 162 | openLuup.find_device {device_type = "TasmotaBridge"} 163 | or 164 | create_TasmotaBridge () 165 | 166 | VIRTUAL[devNo][chdev.bridge.SID].Remote_ID = 7453074 -- 2021.0105.10 ensure ID for "TasmotaBridge" exists 167 | 168 | -- device update: tele/tasmota_7FA953/SENSOR 169 | -- 2021.05.08 add /STATE and /RESULT 170 | local tasmota, mtype = tasmotas: match "^(.-)/(.+)" 171 | 172 | if not (tasmota and VALID[mtype]) then 173 | _log (table.concat ({"Topic ignored", topic, message}, " : ")) 174 | return 175 | end 176 | 177 | local info = json.decode (message) or {[mtype] = message} -- treat invalid JSON as plain text 178 | 179 | local timenow = os.time() 180 | VIRTUAL[devNo].hadevice.LastUpdate = timenow 181 | 182 | local child = devices[tasmota] or init_device (tasmota) 183 | 184 | local DEV = VIRTUAL[child] 185 | DEV.hadevice.LastUpdate = timenow 186 | DEV[tasmota][prefix] = message 187 | 188 | for n,v in pairs (info) do 189 | if type (v) == "table" then 190 | for a,b in pairs (v) do 191 | if type (b) == "table" then 192 | for c,d in pairs(b) do 193 | DEV[n][a .. '/' .. c] = d 194 | end 195 | else 196 | DEV[n][a] = b 197 | end 198 | end 199 | else 200 | DEV[tasmota][n] = v 201 | end 202 | end 203 | end 204 | 205 | 206 | -- startup 207 | function start (config) 208 | config = config or {} 209 | 210 | -- standard prefixes are: {"cmnd", "stat", "tele"} 211 | local prefixes = config.Prefix or "tele, tasmota/tele" -- subscribed Tasmota prefixes 212 | 213 | for prefix in prefixes: gmatch "[^%s,]+" do 214 | luup.register_handler ("Tasmota_MQTT_Handler", "mqtt:" .. prefix .. "/#", prefix) -- * * * MQTT wildcard subscription * * * 215 | end 216 | 217 | -- standard topics are: {"SENSOR", "STATE", "RESULT", "LWT"} -- thanks @Buxton for LWT as text message 218 | local topics = config.Topic or "SENSOR, STATE, RESULT, LWT" 219 | for topic in topics: gmatch "[^%s,]+" do 220 | VALID[topic] = 1 221 | end 222 | 223 | end 224 | 225 | ----- 226 | -------------------------------------------------------------------------------- /openLuup/api.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "openLuup.api", 3 | VERSION = "2023.01.01", 4 | DESCRIPTION = "openLuup object-level API", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2023 AKBooer", 7 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 8 | DEBUG = false, 9 | LICENSE = [[ 10 | Copyright 2013-2023 AK Booer 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | ]] 24 | } 25 | 26 | -- 27 | -- openLuup API - the object-oriented interface 28 | -- 29 | -- the intention is to deprecate the traditional luup.xxx API for new development 30 | -- whilst retaining the original for compatibility with legacy plugins and code. 31 | -- 32 | -- device variable and attributes, plus other system variables and attributes 33 | -- (like cpu and wall-clock times) are directly accessible as API variables. 34 | 35 | 36 | -- 2021.04.27 key parts extracted from luup.lua 37 | 38 | -- 2023.01.01 fix phantom variable creation when setting attribute 39 | 40 | ----- 41 | -- 42 | -- openLuup.cpu_table, 2020.06.28 43 | -- openLuup.wall_table, 2020.06.29 44 | -- 45 | -- returns an object with current plugin CPU / WALL-CLOCK times 46 | -- allow pretty printing and the difference '-' operator 47 | 48 | 49 | local tables = require "openLuup.servertables" -- SID used in device variable virtualization 50 | local chdev = require "openLuup.chdev" 51 | local devutil = require "openLuup.devices" 52 | local sceneutil = require "openLuup.scenes" 53 | local timers = require "openLuup.timers" 54 | 55 | 56 | local devices = devutil.device_list 57 | local scenes = sceneutil.scene_list 58 | local rooms = luup.rooms 59 | 60 | ----- 61 | -- 62 | -- openLuup as an iterator for devices / scenes / ... 63 | -- 64 | -- usage is: for n, d in openLuup "devices" -- or "scenes" 65 | -- 66 | local function api_iterator (self, what) 67 | local possible = {devices = devices, scenes = scenes, rooms = rooms} 68 | return next, possible[what] 69 | end 70 | 71 | local function readonly (_, x) error ("ERROR - READONLY: attempt to create index " .. x, 2) end 72 | 73 | ----- 74 | -- 75 | -- virtualization of device variables 76 | -- 2021.04.25 functionality added to openLuup structure itself 77 | -- 78 | 79 | local SID = tables.SID 80 | local attr_alias = {attr = true, attributes = true} -- pseudo serviceId for virtual devices 81 | 82 | 83 | local api_meta = {__newindex = readonly, __call = api_iterator} 84 | 85 | function api_meta:__index (dev) 86 | 87 | if not devices[dev] then return end -- don't create anything for non-existent device 88 | 89 | local dev_meta = {__newindex = readonly} 90 | 91 | function dev_meta:__index (sid) 92 | sid = SID[sid] or sid -- handle possible serviceId aliases (see servertables.SID) 93 | 94 | local svc_meta = {} 95 | 96 | -- function svc_meta:__call (action) 97 | -- return function (args) 98 | -- local d = devices[dev] 99 | -- if d then 100 | -- return d: call_action (sid, action, args) 101 | -- else 102 | -- return nil, "no such device #" .. tostring(dev) 103 | -- end 104 | -- end 105 | -- end 106 | 107 | function svc_meta:__call (action) 108 | return function (args) 109 | return luup.call_action (sid, action, args, dev) 110 | end 111 | end 112 | 113 | function svc_meta:__index (var) 114 | local d = devices[dev] 115 | if attr_alias[sid] then return d.attributes[var] end 116 | local v = d: variable_get (sid, var) or {} 117 | return v.value, v.time 118 | end 119 | 120 | function svc_meta:__newindex (var, new) 121 | local d = devices[dev] 122 | if attr_alias[sid] then 123 | d.attributes[var] = new 124 | else 125 | new = tostring(new) 126 | local old = self[var] 127 | if old ~= new then 128 | d: variable_set (sid, var, new, true) -- not logged, but 'true' enables variable watch 129 | end 130 | end 131 | end 132 | 133 | return setmetatable({}, svc_meta) 134 | end 135 | 136 | local d = setmetatable ({}, dev_meta) 137 | rawset (self, dev, d) 138 | return d 139 | end 140 | 141 | ----- 142 | -- create module 143 | -- creation of devices / scenes / rooms 144 | -- 145 | 146 | local c_meta = {__newindex = readonly } 147 | 148 | local c_what = {} 149 | 150 | -- device create 151 | -- 152 | -- parameter names are all device ATTRIBUTES 153 | function c_what.device (x) 154 | local dno, dev = chdev.create_device ( 155 | x.device_type, 156 | x.altid, 157 | x.name, 158 | x.device_file, 159 | x.impl_file, 160 | x.ip, 161 | x.mac, 162 | x.hidden, 163 | x.invisible, 164 | x.id_parent, 165 | x.room, 166 | x.plugin, 167 | x.statevariables) 168 | luup.devices[dno] = dev 169 | return dno 170 | end 171 | 172 | function c_meta:__call (what) 173 | local errmsg = 'undefined openLuup.create "%s"' 174 | local this = c_what[what: lower()] 175 | if not this then error (errmsg: format (tostring(what)), 2) end 176 | return this 177 | end 178 | 179 | ----- 180 | -- 181 | -- servers module 182 | -- 183 | 184 | local s_meta = {__newindex = readonly} 185 | 186 | ----- 187 | -- 188 | -- timers module 189 | -- 190 | 191 | local function pcheck (p) 192 | if type (p) ~= "table" then error ("parameter type should be table, but is: " .. type(p), 3) end 193 | end 194 | 195 | local t_meta = {__newindex = readonly} 196 | 197 | local t_call = {} 198 | -- delay = {"callback", "delay","parameter", "name"}, 199 | function t_call.delay (p) 200 | pcheck (p) 201 | return timers.call_delay (p.callback, p.delay, p.parameter, p.name) 202 | end 203 | 204 | -- timer = {"callback", "type", "time", "days", "parameter", "recurring"}, 205 | -- Type is 1=Interval timer, 2=Day of week timer, 3=Day of month timer, 4=Absolute timer. 206 | -- For a day of week timer, Days is a comma separated list with the days of the week where 1=Monday and 7=Sunday. 207 | -- Time is the time of day in hh:mm:ss format. 208 | function t_call.timer (p) 209 | local ttype = {interval = 1, day_of_week = 2, day_of_month = 3, absolute = 4} 210 | pcheck (p) 211 | local ptype = ttype[p.type] or p.type 212 | return timers.call_timer (p.callback, ptype, p.time, p.days, p.parameter, p.recurring) 213 | end 214 | 215 | function t_meta:__call (what) 216 | local this = t_call[what] 217 | if not this then error ("undefined openLuup.timer function: " .. what, 2) end 218 | return this 219 | end 220 | 221 | local t_var = { 222 | cpu = "cpu_clock", 223 | gmt_offset = "gmt_offset", 224 | --loadtime = special case, since it's a constant 225 | night = "is_night", 226 | now = "timenow", 227 | sunrise = "sunrise", 228 | sunset = "sunset", 229 | wall = "timenow", 230 | } 231 | 232 | function t_meta:__index (what) 233 | if what == "loadtime" then return timers.loadtime end 234 | local this = t_var[what] 235 | if not this then error ("undefined openLuup.timer variable: " .. what, 2) end 236 | return timers[this] () -- convert timers module function call into variable value 237 | end 238 | 239 | 240 | ----- 241 | -- 242 | -- export values and methods 243 | -- 244 | local api = { 245 | 246 | create = setmetatable ({}, c_meta), 247 | 248 | servers = setmetatable ({}, s_meta), 249 | timers = setmetatable ({}, t_meta), 250 | 251 | } 252 | 253 | return setmetatable (api, 254 | -- { 255 | -- __newindex = readonly, 256 | -- __call = api_iterator, 257 | -- } 258 | api_meta 259 | ) -- enable virtualization of device variables and actions 260 | 261 | ----- 262 | -------------------------------------------------------------------------------- /openLuup/backup.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | ABOUT = { 6 | NAME = "backup.sh", 7 | VERSION = "2018.07.28", 8 | DESCRIPTION = "user_data backup script /etc/cmh-ludl/cgi-bin/cmh/backup.sh", 9 | AUTHOR = "@akbooer", 10 | COPYRIGHT = "(c) 2013-2018 AKBooer", 11 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 12 | LICENSE = [[ 13 | Copyright 2013-2018 AK Booer 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | ]] 27 | } 28 | 29 | local DIRECTORY_DEFAULT = "backup" -- default backup directory 30 | 31 | -- WSAPI Lua implementation of backup.sh 32 | -- backup written to ./backups/backup.openLuup-AccessPt-YYYYY-MM-DD 33 | 34 | -- 2016.12.10 initial version 35 | -- 2016.06.30 use new compression module to reduce backup file size. 36 | -- 2016.07.12 return HTML page with download link 37 | -- 2016.07.17 add title to HTML page 38 | -- 2016.10.27 changed formatting of backup message to handle fractional file sizes 39 | -- 2016.12.10 use directory path from openLuuup system attribute 40 | 41 | -- 2018.07.12 add &retrieve=filename option (for console page) 42 | -- 2018.07.28 use wsapi.response library 43 | 44 | -- 2019.07.17 use new HTML factory method 45 | 46 | 47 | local userdata = require "openLuup.userdata" 48 | local compress = require "openLuup.compression" 49 | local wsapi = require "openLuup.wsapi" -- for the require and response library methods 50 | local lfs = require "lfs" 51 | local xml = require "openLuup.xml" 52 | 53 | 54 | local _log -- defined from WSAPI environment as wsapi.error:write(...) in run() method. 55 | 56 | 57 | -- global entry point called by WSAPI connector 58 | 59 | --[[ 60 | 61 | The environment is a Lua table containing the CGI metavariables (at minimum the RFC3875 ones) plus any 62 | server-specific metainformation. It also contains an input field, a stream for the request's data, 63 | and an error field, a stream for the server's error log. 64 | 65 | The input field answers to the read([n]) method, where n is the number 66 | of bytes you want to read (or nil if you want the whole input). 67 | 68 | The error field answers to the write(...) method. 69 | 70 | return values: the HTTP status code, a table with headers, and the output iterator. 71 | 72 | --]] 73 | 74 | function run (wsapi_env) 75 | _log = function (...) wsapi_env.error:write(...) end -- set up the log output, note colon syntax 76 | 77 | local req = wsapi.request.new(wsapi_env) 78 | local res = wsapi.response.new () 79 | 80 | local DIRECTORY = (luup.attr_get "openLuup.Backup.Directory") or DIRECTORY_DEFAULT 81 | lfs.mkdir (DIRECTORY) 82 | 83 | local function backup () 84 | local PK = userdata.attributes.PK_AccessPoint or "AccessPt" 85 | local DATE = os.date "%Y-%m-%d" or "0000-00-00" 86 | local fmt = "%s/backup.openLuup-%s-%s.lzap" 87 | local fname = fmt: format (DIRECTORY: gsub('/$',''), PK, DATE) 88 | _log ("backing up user_data to " .. fname) 89 | 90 | local ok, msg = userdata.json (nil) -- save current luup environment 91 | local small -- compressed file 92 | if ok then 93 | local f 94 | f, msg = io.open (fname, 'wb') 95 | if f then 96 | local codec = compress.codec (nil, "LZAP") -- full binary codec with header text 97 | small = compress.lzap.encode (ok, codec) 98 | f: write (small) 99 | f: close () 100 | ok = #ok / 1000 -- convert to file sizes 101 | small = #small / 1000 102 | else 103 | ok = false 104 | end 105 | end 106 | 107 | local h = xml.createHTMLDocument "Backup" 108 | if ok then 109 | msg = ("%0.0f kb compressed to %0.0f kb (%0.1f:1)") : format (ok, small, ok/small) 110 | h.body: appendChild { 111 | h.div { 112 | "backup completed: ", h.p (msg), 113 | "written to ", h.b (fname), 114 | h.p {h.a {href="../../".. fname, download=fname, type="application/octet-stream", "DOWNLOAD"}}} 115 | } 116 | else 117 | res.status = 500 118 | h.body: appendChild {h.div {"backup failed: ", msg} } 119 | end 120 | _log (msg) 121 | res.content_type = "text/html" 122 | res: write (tostring (h)) 123 | end 124 | 125 | -- retrieve the contents of a backup file, uncompressing if necessary 126 | local function retrieveFile (file) 127 | local fname = table.concat {DIRECTORY: gsub('/$',''), '/', file} 128 | local f, err = io.open (fname, 'rb') 129 | if f then 130 | local code = f: read "*a" 131 | f: close () 132 | 133 | if file: match "%.lzap$" then -- it's a compressed user_data file 134 | local codec = compress.codec (nil, "LZAP") -- full-width binary codec with header text 135 | code = compress.lzap.decode (code, codec) -- uncompress the file 136 | end 137 | 138 | res: write (code) 139 | res.content_type = "application/json" 140 | else 141 | res.status = 404 142 | res: write (err or "Unknown error opening file") 143 | end 144 | end 145 | 146 | ----------- 147 | 148 | local retrieve = req.GET.retrieve -- &retrieve=filename option 149 | 150 | if retrieve then 151 | retrieveFile (retrieve) 152 | else 153 | backup () 154 | end 155 | 156 | return res: finish () 157 | end 158 | 159 | ----- 160 | -------------------------------------------------------------------------------- /openLuup/client.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "openLuup.client", 3 | VERSION = "2019.10.14", 4 | DESCRIPTION = "luup.inet .wget() and .request()", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2019 AKBooer", 7 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 8 | DEBUG = false, 9 | LICENSE = [[ 10 | Copyright 2013-2019 AK Booer 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | ]] 24 | } 25 | 26 | -- 27 | -- openLuup CLIENT -- 28 | -- 29 | -- supports Basic and Digest authentication over HTTP / HTTPS 30 | -- 31 | 32 | 33 | -- 2019.07.30 split from openLuup.server 34 | -- 2019.10.14 added start() function, called from server, to allow use of arbitrary port 35 | 36 | 37 | local url = require "socket.url" 38 | local http = require "socket.http" 39 | local https = require "ssl.https" 40 | local ltn12 = require "ltn12" -- for wget handling 41 | --local mime = require "mime" -- for basic authorization in wget 42 | 43 | local OKmd5,md5 = pcall (require, "md5") -- for digest authenication (may be missing) 44 | 45 | local logs = require "openLuup.logs" 46 | local tables = require "openLuup.servertables" -- mimetypes and status_codes 47 | local servlet = require "openLuup.servlet" 48 | local wsapi = require "openLuup.wsapi" -- to build WSAPI request environment 49 | 50 | -- local _log() and _debug() 51 | local _log, _debug = logs.register (ABOUT) 52 | 53 | -- CONFIGURATION DEFAULTS 54 | 55 | local PORT -- filled in during start() 56 | local myIP = tables.myIP 57 | 58 | -- local functions 59 | 60 | local parse_header = function(h) 61 | local r = {} 62 | for k,v in h: gmatch '(%w+)="?([^",]+)' do r[k:lower()] = v end 63 | return r 64 | end 65 | 66 | local function make_digest_header(t) 67 | local s = {"Digest "} 68 | local function p(...) for _,x in ipairs {...} do s[#s+1] = x end; end 69 | for n, v in pairs (t) do p (n, '="', v, '"', ', ') end -- unquote nc ??? 70 | s[#s] = nil 71 | return table.concat(s) 72 | end 73 | 74 | local function hash(...) return md5.sumhexa(table.concat({...}, ":")) end 75 | 76 | ---------------------------------------------------- 77 | -- 78 | -- Digest authorization code inspired by: https://github.com/catwell/lua-http-digest 79 | -- as suggested by @jswim77 here: http://forum.micasaverde.com/index.php/topic,63465.msg380840.html#msg380840 80 | -- or, in the new Vera Community forum: https://community.getvera.com/t/openluup-cameras/198812/36 81 | -- and prototyped by @rafale77 here: https://github.com/akbooer/openLuup/pull/11 82 | -- 83 | -- this requires the lua-md5 module to be on the Lua path 84 | -- 85 | local _request = function(t, Timeout) 86 | local URL = url.parse(t.url) 87 | local user, password = URL.user, URL.password -- may or may not be present 88 | local scheme = URL.scheme == "https" and https or http 89 | scheme.TIMEOUT = Timeout or 5 90 | 91 | -- TODO: limited number of redirects 92 | local b, c, h = scheme.request(t) -- if user/password then this tries Basic authorization 93 | if (c == 401) and h["www-authenticate"] then -- else try digest 94 | local ht = parse_header(h["www-authenticate"]) 95 | 96 | assert(ht.realm and ht.nonce, "missing realm or nonce in received WWW-Authenticate header") 97 | if not OKmd5 then 98 | return nil, "MD5 module not available for digest authorization" 99 | end 100 | if ht.qop ~= "auth" then 101 | return nil, string.format("unsupported qop (%s)", tostring(ht.qop)) 102 | end 103 | if ht.algorithm and (ht.algorithm:lower() ~= "md5") then 104 | return nil, string.format("unsupported algorithm (%s)", tostring(ht.algorithm)) 105 | end 106 | 107 | local nc, cnonce = "00000001", ("%08x"): format (os.time()) 108 | local uri = url.build{path = URL.path, query = URL.query} 109 | local method = t.method or "GET" 110 | local response = hash( 111 | hash(user or '', ht.realm, password or ''), 112 | ht.nonce, 113 | nc, 114 | cnonce, 115 | "auth", 116 | hash(method, uri) 117 | ) 118 | 119 | t.headers = t.headers or {} 120 | t.headers.authorization = make_digest_header { 121 | username = user, 122 | realm = ht.realm, 123 | nonce = ht.nonce, 124 | uri = uri, 125 | cnonce = cnonce, 126 | nc = nc, 127 | qop = "auth", 128 | algorithm = "MD5", 129 | response = response, 130 | opaque = ht.opaque, 131 | } 132 | 133 | b, c, h = scheme.request(t) 134 | end 135 | return b, c, h 136 | end 137 | 138 | local function request (x, Timeout) 139 | local response = {} 140 | local b, c, h = _request ({url = x, sink = ltn12.sink.table(response)}, Timeout) 141 | b = (b == 1) and table.concat(response) or b 142 | return b, c, h 143 | end 144 | 145 | 146 | ---------------------------------------------------- 147 | -- 148 | -- HTTP CLIENT request (for luup.inet.wget) 149 | -- 150 | 151 | local self_reference = { 152 | ["localhost"] = true, 153 | ["127.0.0.1"] = true, 154 | ["0.0.0.0"] = true, 155 | [myIP] = true, 156 | } 157 | 158 | --[[ see: http://wiki.micasaverde.com/index.php/Luup_Lua_extensions#function:_wget 159 | 160 | This reads the URL and returns 3 variables: the first is a numeric error code which is 0 if successful. 161 | The second variable is a string containing the contents of the page. 162 | The third variable is the HTTP status code. 163 | If Timeout is specified, the function will timeout after that many seconds. 164 | The default value for Timeout is 5 seconds. 165 | If Username and Password are specified, they will be used for HTTP Basic Authentication. 166 | 167 | --]] 168 | 169 | -- issue a GET request, handling local ones for port 3480 without going over HTTP 170 | local function wget (request_URI, Timeout, Username, Password) 171 | local result, status 172 | local responseHeaders 173 | 174 | local relative = request_URI: match "^/[^/]" -- 2018.03.15 it's a relative URL, must be served from here 175 | if not relative then 176 | if not (request_URI: match "^%w+://") then 177 | request_URI = "http://" .. request_URI -- assume it's an external HTTP request 178 | end 179 | end 180 | 181 | local URL = url.parse (request_URI) -- parse URL 182 | 183 | local self_ref = self_reference [URL.host] and URL.port == PORT -- 2016-03-16 check for port #, thanks @reneboer 184 | if relative or self_ref then 185 | 186 | -- INTERNAL request 187 | local headers, iterator 188 | URL.path = URL.path:gsub ("/port_3480", '') -- 2016.09.16, thanks @explorer 189 | local wsapi_env = wsapi.make_env (URL.path, URL.query) 190 | status, headers, iterator = servlet.execute (wsapi_env) -- make the request call 191 | result = {} 192 | for x in iterator do result[#result+1] = tostring(x) end -- build the return string 193 | result = table.concat (result) 194 | 195 | else 196 | 197 | -- EXTERNAL request OR not port 3480 198 | 199 | -- Username and Password parameters override either of those in the URL 200 | Username, Password = Username or URL.user, Password or URL.password 201 | URL.user, URL.password = Username, Password 202 | 203 | URL = url.build (URL) -- reconstruct request for external use 204 | _debug (URL) 205 | 206 | result, status, responseHeaders = request (URL, Timeout) 207 | end 208 | 209 | if not result then -- 2019.07.30 socket library has failed somehow, fix up error and message 210 | result = table.concat {status or "unknown error in socket library", ": ", request_URI} 211 | status = -1 212 | end 213 | 214 | local wget_status = status -- wget has a strange return code 215 | if status == 200 then 216 | wget_status = 0 217 | else -- 2017.05.05 add error logging 218 | local error_message = "WGET error status: %s, request: %s" -- 2017.05.25 fix wget error logging format 219 | _log (error_message: format (status, request_URI)) 220 | end 221 | -- note reversal of first two parameters order cf. http.request() 222 | return wget_status, result or '', status, responseHeaders 223 | end 224 | 225 | 226 | --------------------------------------------- 227 | -- 228 | -- TEST digest authentication 229 | -- 230 | --[[ 231 | local pretty = require "pretty" 232 | 233 | print(pretty {luup.inet.wget "httpbin.org/auth"}) 234 | print(pretty {luup.inet.wget ("http://foo:garp@httpbin.org/basic-auth/foo/garp")}) 235 | print(pretty {luup.inet.wget ("http://httpbin.org/basic-auth/foo/garp", 5, "foo", "garp")}) 236 | print(pretty {luup.inet.wget ("http://httpbin.org/digest-auth/auth/foo/garp", 5, "foo", "garp")}) 237 | 238 | --]] 239 | -- 240 | --------------------------------------------- 241 | 242 | --- return module variables and methods 243 | return { 244 | ABOUT = ABOUT, 245 | 246 | -- constants 247 | myIP = myIP, 248 | 249 | -- functions 250 | wget = wget, 251 | start = function (config) PORT = tostring(config.Port or 3480) end, -- 2019.10.14 252 | 253 | -- TODO: inet.request 254 | } 255 | 256 | ----- 257 | -------------------------------------------------------------------------------- /openLuup/compression.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "openLuup.compression", 3 | VERSION = "2016.06.30", 4 | DESCRIPTION = "Data compression using LZAP", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2016 AKBooer", 7 | DOCUMENTATION = "http://read.pudn.com/downloads167/ebook/769449/dataCompress.pdf", 8 | LICENSE = [[ 9 | Copyright 2016 AK Booer 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | ]] 23 | } 24 | 25 | 26 | -- CODEC 27 | 28 | --[[ 29 | 30 | This codec module does bi-directional translations of integer arrays of codewords <---> little-endian byte strings. 31 | Maximum word count is determined by the code alphabet used by the byte stream (fixed at two bytes per word.) 32 | If invoked without a parameter, the code alphabet is the full 0x00 - 0xFF range per byte, giving 16-bits per word. 33 | 34 | An alternative pre-defined alphabet is provided by the module: the json_alphabet, being the 92 non-escaped JSON 35 | string characters (some ambiguity about the '/' character, so that is excluded.) Using this alphabet ensures 36 | a coded byte stream which may be used as a directly coded JSON string with no escaped expansions, but limits 37 | the available codes to 92 * 92 = 8464 (cf. 65536 for the full byte range.) 38 | 39 | --]] 40 | 41 | --note that ASCII printable characters are 0x20 - 0x7E (0x7F is 'delete') 42 | --note that XML quoted characters are: < > " ' & 43 | --note that JSON quoted printable characters are: " \ / (or possibly not /) 44 | 45 | local unescaped_JSON_alphabet = 46 | [==[ !#$%&'()*+,-.0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~]==] 47 | 48 | local full_alphabet = '' -- empty string forces full-width alphabet 49 | 50 | -- null encoder (returns words, not byte string) 51 | local null_codec = 52 | { 53 | alphabet = {}, 54 | symbols = 2^53, -- note that 2^53 is the highest integer that a 64-bit IEEE floating-point number can represent 55 | 56 | encode = function (x) return x end, 57 | decode = function (x) return x end, 58 | } 59 | 60 | -- optional header is prefix to encoded byte stream 61 | -- error raised if not found at start of decode byte stream 62 | local function codec (code_alphabet, header) 63 | if code_alphabet == null_codec then return null_codec end 64 | header = header or '' 65 | if not code_alphabet or code_alphabet == full_alphabet then -- use two full-width bytes to represent a word 66 | code_alphabet = {} 67 | for i = 0, 0xFF do code_alphabet[i+1] = string.char (i) end 68 | code_alphabet = table.concat (code_alphabet) 69 | end 70 | 71 | local LSB, MSB = {}, {} -- lookup table to convert characters to lsb/msb numeric values 72 | local alpha = {} -- breaks the alphabet into separate characters 73 | local i, base = 0, #code_alphabet 74 | for c in code_alphabet: gmatch "." do 75 | LSB[c] = i -- NB: the first code represents ZERO! 76 | MSB[c] = base * i 77 | i = i + 1 78 | alpha[i] = c 79 | end 80 | 81 | -- encodes a word array into byte-pair string 82 | local function encode (words) 83 | local bytes = {header} 84 | local base = #code_alphabet 85 | for _, word in ipairs (words) do 86 | local lsb = word % base + 1 87 | local msb = math.floor (word / base) + 1 88 | bytes[#bytes+1] = alpha[lsb] 89 | bytes[#bytes+1] = alpha[msb] 90 | end 91 | return table.concat(bytes) 92 | end 93 | 94 | -- converts an array of little-endian byte-pairs into words 95 | local function decode (bytes) 96 | local words = {} 97 | assert (bytes: sub(1,#header) == header, "byte stream header mismatch") 98 | for n = #header+1, #bytes, 2 do 99 | local lsb = bytes:sub (n,n) 100 | local m = n+1 101 | local msb = bytes:sub (m,m) 102 | words[#words+1] = MSB[msb] + LSB[lsb] 103 | end 104 | return words 105 | end 106 | 107 | return { 108 | alphabet = alpha, -- byte code alphabet as an array of characters 109 | symbols = (#alpha) ^2, -- number of possible symbols in byte-pair code 110 | 111 | encode = encode, 112 | decode = decode, 113 | } 114 | 115 | end 116 | 117 | -- DICTIONARY 118 | -- No re-cycling of dictionary entries is currently used. 119 | 120 | local function dictionary (max_size) -- bi-directional lookup 121 | 122 | local dict = {} 123 | local N -- dictionary length 124 | local MAX_WORD = 128 -- a good compromise 125 | 126 | local function add (prev, word) 127 | local both = (prev .. word): sub(1, MAX_WORD) 128 | if N + #both > max_size then return end 129 | for i = #prev+1 ,#both do 130 | local x = both:sub(1,i) 131 | if not dict[x] then 132 | N = N + 1 133 | dict[x] = N 134 | dict[N] = x 135 | end 136 | end 137 | end 138 | 139 | -- initialise dictionary with all possible byte-codes 140 | N = 256 141 | for i = 1,N do 142 | local c = string.char(i-1) 143 | dict[c] = i 144 | dict[i] = c 145 | end 146 | 147 | return { 148 | add = add, 149 | lookup = function (x) return dict[x] end, 150 | } 151 | end 152 | 153 | -- 154 | -- LZAP compression 155 | -- 156 | 157 | -- compession algorithm 158 | local function encode (text, codec, dict) 159 | codec = codec or null_codec 160 | dict = dictionary (codec.symbols) 161 | 162 | local add = dict.add 163 | local lookup = dict.lookup 164 | local prev, word = '' 165 | local code = {} 166 | 167 | local m = 1 168 | for n = 1,#text do 169 | -- if n % 1e4 == 0 then print (("%6d %0.1f%%"): format (n,1e2*#code/n)) end -- monitor compression rate 170 | local new = text: sub(m,n) 171 | if not lookup (new) then 172 | code[#code+1] = lookup (word) 173 | add (prev, word) 174 | prev = word 175 | new = new: sub(-1,-1) 176 | m = n 177 | end 178 | word = new 179 | end 180 | code[#code+1] = lookup (word) 181 | return codec.encode (code) -- turn code words into byte string 182 | end 183 | 184 | -- decompression 185 | local function decode (code, codec, dict) 186 | codec = codec or null_codec 187 | code = codec.decode (code) -- turn byte string into code words 188 | dict = dictionary (codec.symbols) 189 | 190 | local add = dict.add 191 | local lookup = dict.lookup 192 | local prev, word = '' 193 | local text = {} 194 | 195 | for n = 1, #code do 196 | word = lookup (code[n]) 197 | add (prev, word) 198 | text[#text+1] = word 199 | prev = word 200 | end 201 | return table.concat (text) 202 | end 203 | 204 | 205 | ----- 206 | 207 | return { 208 | ABOUT = ABOUT, 209 | 210 | codec = setmetatable ({ -- this syntax allows both codec() and codec.new() calls 211 | json = unescaped_JSON_alphabet, -- also enables parameter self-reference: codec(codec.json) 212 | full = full_alphabet, -- full two-byte alphabet: codec (codec.full) 213 | null = null_codec, 214 | new = codec, 215 | },{__call = function (_, ...) return codec (...) end}), 216 | 217 | dictionary = dictionary, 218 | 219 | lzap = { 220 | encode = encode, 221 | decode = decode, 222 | }, 223 | } 224 | 225 | ----- 226 | -------------------------------------------------------------------------------- /openLuup/logs.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "openLuup.logs", 3 | VERSION = "2018.03.25", 4 | DESCRIPTION = "basic log file handling, including versioning", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2018 AKBooer", 7 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 8 | LICENSE = [[ 9 | Copyright 2013-2018 AK Booer 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | ]] 23 | } 24 | 25 | -- log handling - basic at the moment 26 | -- 27 | -- 2016.05.14 add logging for AltUI workflows 28 | -- 2016.05.26 fix error on nil message 29 | -- 2016.06.09 fix numeric message error 30 | -- 2016.08.01 truncate long variable values in AltUI log 31 | -- 2016.11.18 convert parameter to string in truncate 32 | -- 2016.12.05 add log file configurations parameters (thanks @logread) 33 | -- see: http://forum.micasaverde.com/index.php/topic,34476.msg300645.html#msg300645 34 | 35 | -- 2018.02.06 fixed missing tab in AltUI scene log (thanks @kartcon, @amg0) 36 | -- see: http://forum.micasaverde.com/index.php/topic,56847.0.html 37 | -- 2018.02.08 use actual scene table hex address rather than <0x0> in AltUI log 38 | -- 2018.03.15 provide reqistration for modules giving customised _log() and _debug() functions 39 | -- 2018.03.25 _debug() goes to stdout AND log file 40 | 41 | 42 | local socket = require "socket" 43 | local lfs = require "lfs" -- for creating default log firectory 44 | 45 | local start_time = os.time() 46 | 47 | -- openLuup configuration options: 48 | local Logfile = "logs/LuaUPnP.log" -- full path to log file 49 | local LogfileLines = 2000 -- number of logfile lines before rotation 50 | local LogfileVersions = 5 -- number of versions to retain 51 | 52 | local StartupLogfile = "logs/LuaUPnP_startup.log" 53 | 54 | --[[ 55 | 56 | Vera records different types of log entries, in its log files, according to logging levels. By default only log levels 1-10 will make it to /var/log/cmh/LuaUPnP.log and all other types of messages are discarded. 57 | 58 | To add more log levels edit the file /etc/cmh/cmh.conf. To see all log entries check the verbose option on Advanced/Logs, or put a comment character (#) in front of the LogLevels= line in /etc/cmh/cmh.conf 59 | 60 | #LogLevels = 1,2,3,4,5,6,7,8,9,50,40 61 | 62 | Here's a full list of the log types that Vera supports: 63 | 64 | LV_CRITICAL 1 65 | LV_WARNING 2 66 | LV_STARTSTOP 3 67 | LV_JOB 4 68 | LV_HA 5 69 | LV_VARIABLE 6 70 | LV_EVENT 7 71 | LV_ACTION 8 72 | LV_ENUMERATION 9 73 | LV_STATUS 10 74 | LV_CHILD_DEVICES 11 75 | LV_DATA_REQUEST 12 76 | 77 | LV_LOCKING 20 78 | LV_IR 28 79 | LV_ALARM 31 80 | LV_SOCKET 32 81 | LV_DEBUG 35 82 | LV_PROFILER 37 83 | LV_PROCESSUTILS 38 84 | 85 | // Z-Wave starts with 4 86 | LV_ZWAVE 40 87 | LV_SEND_ZWAVE 41 88 | LV_RECEIVE_ZWAVE 42 89 | 90 | // Lua starts with 5 91 | LV_LUA 50 92 | LV_SEND_LUA 51 93 | LV_RECEIVE_LUA 52 94 | 95 | // Insteon starts with 6 96 | LV_INSTEON 60 97 | LV_SEND_INSTEON 61 98 | LV_RECEIVE_INSTEON 62 99 | 100 | // Low level debugging starts with 2+ ZWave/Lua/Insteon 101 | LV_ZWAVE_DEBUG 24 102 | LV_LUA_DEBUG 25 103 | LV_INSTEON_DEBUG 26 104 | 105 | --]] 106 | 107 | 108 | -- return formatted current time (or given time) as a string 109 | -- ISO 8601 date/time: YYYY-MM-DDThh:mm:ss or other specified format 110 | local function formatted_time (date_format, now) 111 | now = now or socket.gettime() -- millisecond resolution 112 | date_format = date_format or "%Y-%m-%dT%H:%M:%S" -- ISO 8601 113 | local date = os.date (date_format, math.floor (now)) 114 | local ms = math.floor (1000 * (now % 1)) 115 | return ('%s.%03d'):format (date, ms) 116 | end 117 | 118 | -- shorten long variable strings, removing control characters 119 | local function truncate (text) 120 | text = (tostring(text)): gsub ("%c", ' ') 121 | if #text > 120 then text = text: sub (1,115) .. "..." end -- truncate long variable values 122 | return text 123 | end 124 | 125 | 126 | -- dummy io module for missing files 127 | local function dummy_io (functions) 128 | local function noop () end 129 | functions = functions or {} 130 | return { 131 | write = functions.write or noop, 132 | close = functions.close or noop, 133 | setvbuf = functions.setvbuf or noop, 134 | } 135 | end 136 | 137 | -- 138 | -- log message to luup.log file 139 | -- 140 | 141 | -- logfile () open new logfile with a number of archived versions, and lines per version 142 | -- returns table with send function to actually log new data 143 | local function openLuup_logger (info) 144 | local f 145 | local logfile_name, versions, maxLines = info.name, info.versions or 0, info.lines or LogfileLines 146 | local N = 0 -- current line number 147 | local formatted_time = formatted_time 148 | local altui = info.altui 149 | 150 | -- open log 151 | local function open_log () 152 | local function print_not_write (self, msg) -- in case there's a problem opening file 153 | print (msg:gsub ("%s+",' ')) 154 | end 155 | local f = io.open (logfile_name, 'w') or dummy_io {write = print_not_write} 156 | f:setvbuf "line" 157 | return f 158 | end 159 | 160 | -- rename old files 161 | local function rename_files () 162 | for i = versions-1,1,-1 do 163 | os.rename (logfile_name..'.'..(i), logfile_name..'.'..(i+1)) 164 | end 165 | os.rename (logfile_name, logfile_name..'.'..(1)) 166 | end 167 | 168 | -- rotate files, possibly having been given new file info 169 | local function rotate (info) 170 | info = info or {} 171 | logfile_name = info.Name or logfile_name 172 | versions = info.Versions or versions 173 | maxLines = info.Lines or maxLines 174 | local runtime = (os.time() - start_time) / 60 / 60 / 24 175 | local fmt = "%s :: openLuup LOG ROTATION :: (runtime %0.1f days) \n" 176 | local message = fmt: format (formatted_time "%Y-%m-%d %H:%M:%S", runtime) 177 | f:write (message) 178 | f: close () 179 | rename_files () 180 | f = open_log () 181 | f:write (message) 182 | end 183 | 184 | -- write data 185 | local function write (message) 186 | f:write (message) 187 | N = N + 1 188 | if (N % maxLines) == 0 then rotate () end 189 | end 190 | 191 | -- format and write log 192 | local function send (msg, subsystem_or_number, devNo) 193 | msg = tostring (msg) 194 | if msg: match "Wkflow %- Workflow: %d+%-%d+," then -- copy AltUI Workflow message to altui log 195 | altui.workflow (msg, subsystem_or_number, devNo) 196 | end 197 | subsystem_or_number = subsystem_or_number or 50 198 | if type (subsystem_or_number) == "number" then subsystem_or_number = "luup_log" end 199 | local now = formatted_time "%Y-%m-%d %H:%M:%S" 200 | local message = table.concat {now, " ",subsystem_or_number, ":", devNo or '', ": ", tostring(msg), '\n'} 201 | write (message) 202 | end 203 | 204 | -- logfile init 205 | rename_files () -- save the old ones 206 | f = open_log () -- start anew 207 | return {send = send, rotate = rotate} 208 | end 209 | 210 | -- 211 | -- write log for ALTUI to parse: contains only variables and scene runs 212 | -- 213 | -- writes to usual Vera log location: /tmp/logs/cmh/LuaUPnP.log 214 | -- 215 | 216 | --[[ 217 | Note that ALTUI now parses logs for scene and variable info. 218 | 219 | From @amg0 (personal communication): 220 | 221 | It is based on pattern matching of the logs but done in 2 places. 222 | a first one done by LUA in the Handler, then a second one in javascript to refine/finish the work 223 | 224 | for scene: 225 | in LUA 226 | - var cmd = "cat /var/log/cmh/LuaUPnP.log | grep 'Scene::RunScene running {0}'".format(id); 227 | then the result is searched in Javascript to extract the date/time & scene name with the following regexp 228 | - var re = /\d*\t(\d*\/\d*\/\d*\s\d*:\d*:\d*.\d*).*Scene::RunScene running \d+ (.*) <.*/g; 229 | 230 | 231 | for device variable: 232 | in LUA 233 | - var cmd = "cat /var/log/cmh/LuaUPnP.log | grep 'Device_Variable::m_szValue_set device: {0}.*;1m{1}\033'".format(device.id,state.variable); 234 | then the result is searched in Javascript to extract the date/time & old and new value with the following regexp 235 | - var re = /\d*\t(\d*\/\d*\/\d*\s\d*:\d*:\d*.\d*).*was: (.*) now: (.*) #.*/g; 236 | 237 | 238 | 239 | This means you need to match this: 240 | 241 | for scenes: 242 | this: 243 | 08 07/16/15 16:18:20.649 Scene::RunScene running 3 RGBW Full ON <0x743d4520> 244 | with: 245 | "%d*\t(%d*/%d*/%d*%s%d*:%d*:%d*.%d*).*Scene::RunScene running %d+ (.*) <.*" 246 | 247 | for variables: 248 | this: 249 | 06 07/14/15 15:34:17.485 Device_Variable::m_szValue_set device: 5 service: urn:akbooer-com:serviceId:EventWatcher1 variable: AppMemoryUsed was: 863 now: 943 #hooks: 1 upnp: 0 skip: 0 v:(nil)/NONE duplicate:0 <0x76376520> 250 | with: 251 | "%d*\t(%d*/%d*%/%d*%s%d*:%d*:%d*.%d*).*was: (.*) now: (.*) #.*" 252 | 253 | for workflows: 254 | 255 | the pattern string I am looking for is like that where {0} is a altuiid so something 0-nn 256 | 257 | "cat /var/log/cmh/LuaUPnP.log | grep '[0123456789]: ALTUI: Wkflow - Workflow: {0}, Valid Transition found'".format(altuiid); 258 | 259 | Here are a couple of examples 260 | 261 | luup_log:216: ALTUI: Wkflow - Workflow: 0-2, Valid Transition found:Timer:Retour, Active State:Thingspeak=>Idle <0x7454e520> 262 | luup_log:216: ALTUI: Wkflow - Workflow: 0-3, Valid Transition found:Timer:5s, Active State:Auto Close=>Auto Mode <0x7454e520> 263 | 264 | --]] 265 | 266 | local function altui_logger (info) 267 | local f 268 | local logfile_name, maxLines = info.name, info.lines or LogfileLines 269 | local N = 0 -- current line number 270 | local formatted_time = formatted_time 271 | 272 | -- open log 273 | local function open_log () 274 | local f = io.open (logfile_name, 'w') or dummy_io {} 275 | f:setvbuf "line" 276 | return f 277 | end 278 | 279 | -- rename old files 280 | local function rotate_logs () 281 | os.rename (logfile_name, logfile_name.. ".1") 282 | end 283 | 284 | local function write (message) 285 | f:write (message) 286 | N = N + 1 287 | if (N % maxLines) == 0 then 288 | f: close () 289 | rotate_logs () 290 | f = open_log () 291 | end 292 | end 293 | 294 | local function variable (var) 295 | local now = formatted_time "%m/%d/%y %H:%M:%S" 296 | local vfmt = "%02d\t%s\tDevice_Variable::m_szValue_set device: %d service: %s " .. 297 | "variable: \027[35;1m%s\027[0m was: %s now: %s #hooks: %d \n" 298 | local msg = vfmt: format (6, now, var.dev, var.srv, var.name, 299 | truncate (var.old or "MISSING"), truncate (var.value), #var.watchers) 300 | write (msg) 301 | return msg -- for testing 302 | end 303 | 304 | local function scene (scn) 305 | local now = formatted_time "%m/%d/%y %H:%M:%S" 306 | local sfmt = "%02d\t%s\tScene::RunScene running %d %s <%s>\n" -- 2018.02.06 fixed second tab 307 | local msg = sfmt: format (8, now, scn.id, scn.name, tostring(scn): match "0x%x+") 308 | write (msg) 309 | return msg -- for testing 310 | end 311 | 312 | local function workflow (wrk, level, dev) 313 | local now = formatted_time "%m/%d/%y %H:%M:%S" 314 | local sfmt = "%02d\t%s\tluup_log:%d: %s <%s>\n" 315 | local msg = sfmt: format (level or 50, now, dev or 0, wrk or '?', "0x0") 316 | write (msg) 317 | return msg -- for testing 318 | end 319 | 320 | -- altui_logger init () 321 | 322 | rotate_logs () -- save the old ones 323 | f = open_log () -- start anew 324 | return { 325 | scene = scene, 326 | variable = variable, 327 | workflow = workflow, 328 | } 329 | end 330 | 331 | 332 | -- INIT 333 | 334 | lfs.mkdir "logs" -- default location for startup and regular log 335 | 336 | -- altui log (for variable and scene history) 337 | -- note that altui reads from /var/log/cmh/LuaUPnP.log 338 | local altui = altui_logger { 339 | name = "/var/log/cmh/LuaUPnP.log", 340 | lines = 5000} 341 | 342 | -- openLuup log 343 | local normal = openLuup_logger { 344 | name = StartupLogfile, 345 | versions = LogfileVersions, 346 | lines = LogfileLines, 347 | altui = altui} 348 | 349 | -- display module banner 350 | local function banner (ABOUT) 351 | local msg = ("%" .. 27-#ABOUT.NAME .. "s %s @akbooer"): format ("version", ABOUT.VERSION) 352 | normal.send (msg, ABOUT.NAME, '') 353 | end 354 | 355 | -- module registration 356 | -- ... uses ABOUT information for NAME and DEBUG status, and module version log line 357 | local function register (about) 358 | banner (about) -- for version control 359 | local function _log (msg, name) normal.send (msg, name or about.NAME) end 360 | local function _debug (...) 361 | if about.DEBUG then 362 | print (about.NAME, ...) -- debug to stdout 363 | _log (table.concat ({...}, '\t')) -- debug to log file 364 | end 365 | end 366 | return _log, _debug 367 | end 368 | 369 | -- export methods 370 | 371 | return { 372 | ABOUT = ABOUT, 373 | 374 | banner = banner, 375 | rotate = normal.rotate, 376 | send = normal.send, 377 | altui_variable = altui.variable, 378 | altui_scene = altui.scene, 379 | register = register, 380 | 381 | -- for testing 382 | openLuup_logger = openLuup_logger, 383 | altui_logger = altui_logger, 384 | } 385 | 386 | ---- 387 | -------------------------------------------------------------------------------- /openLuup/panels.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "panels.lua", 3 | VERSION = "2024.04.03", 4 | DESCRIPTION = "built-in console device panel HTML functions", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2024 AKBooer", 7 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 8 | LICENSE = [[ 9 | Copyright 2013-2024 AK Booer 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | ]] 23 | } 24 | 25 | --[[ 26 | 27 | Each device panel is described by a table which may define three functions, 28 | to return the panel as displayed on the device pages, the icon, 29 | and the extended panel seen on the device's control tab page. 30 | 31 | control -- device control tab 32 | panel -- device panel 33 | icon x -- icon 34 | 35 | The tables are index by device type, and called with the device number as a single parameter. 36 | 37 | Each function returns HTML - either plain text or openLuup DOM model - which defines the panel to be drawn 38 | 39 | -- using openLuup XML module and W3.css framework 40 | 41 | --]] 42 | 43 | -- 2019.06.20 @akbooer 44 | -- 2019.07.14 use new HTML constructor methods 45 | -- 2019.11.14 move into separate file 46 | 47 | -- 2020.05.07 add simple camera control panel showing video stream 48 | 49 | -- 2021.01.20 add panel utility functions 50 | -- 2021.03.05 generic Shelly control panel 51 | -- 2021.03.17 generic sensor device (for @ArcherS) 52 | 53 | -- 2022.06.20 fix non-string argument in todate() (thanks @a-lurker) 54 | -- 2022.06.30 ...another go at the above fix 55 | -- 2022.07.31 add ShellyHomePage to Shelly scene controllers 56 | -- 2022.11.11 prettier openLuup device control panel 57 | -- 2022.11.27 add basic support for Zigbee2MQTT bridge 58 | -- 2022.12.30 add link to https://dev.netatmo.com/ 59 | 60 | -- 2023.01.03 add Authorize button for Netatmo Oath2 tokens 61 | 62 | -- 2024.02.27 fix servertables require path 63 | -- 2024.03.26 increase Shelly web page width to 800 from 500 64 | -- 2024.03.30 add panel and control for ShellyBridge, with firmware status 65 | 66 | 67 | local xml = require "openLuup.xml" 68 | local srv = require "openLuup.servertables" 69 | local API = require "openLuup.api" 70 | 71 | local h = xml.createHTMLDocument () -- for factory methods 72 | local div = h.div 73 | local a, p = h.a, h.p 74 | 75 | local sid = srv.SID { 76 | althue = "urn:upnp-org:serviceId:althue1", 77 | camera = "urn:micasaverde-com:serviceId:Camera1", 78 | netatmo = "urn:akbooer-com:serviceId:Netatmo1", 79 | security = "urn:micasaverde-com:serviceId:SecuritySensor1", 80 | weather = "urn:upnp-micasaverde-com:serviceId:Weather1", 81 | } 82 | 83 | local function tiny_date_time (epoch) 84 | local date_time = tonumber(epoch) and os.date ("%Y-%m-%d %H:%M:%S", epoch) or "--- 00:00" 85 | return div {class = "w3-tiny w3-display-bottomright", date_time} 86 | end 87 | 88 | local function ShellyHomePage (devNo) 89 | local ip = luup.attr_get ("ip", devNo) or '' 90 | local src = table.concat {"http://", ip, "/"} 91 | return div {class = "w3-panel", h.iframe {src = src, width="800", height="300"}} 92 | end 93 | 94 | local function link(link) 95 | return h.span {style ="text-align:right", h.a {href= link, title="link", 96 | h.img {height=14, width=14, class="w3-hover-opacity", alt="goto", src="icons/link-solid.svg"}}} 97 | end 98 | 99 | local panels = { 100 | 101 | -- 102 | -- openLuup 103 | -- 104 | openLuup = { 105 | control = function(devNo) 106 | local about = luup.devices[devNo].environment.ABOUT 107 | local forum = about.FORUM 108 | local donate = about.DONATE 109 | return div { 110 | div { 111 | a {class = "w3-round-large w3-dark-gray w3-button w3-margin", href=forum, target="_blank", 112 | h.img {alt="SmartHome Community", width=300, src= forum.. "assets/uploads/system/site-logo.png"}}}, 113 | div { 114 | a {class = "w3-round-large w3-white w3-button w3-margin w3-border", href=donate, target="_blank", 115 | h.img {alt="Donate to Cancer Research", width=300, 116 | src= "https://www.cancerresearchuk.org/sites/all/themes/custom/cruk/cruk-logo.svg"}}}, 117 | } 118 | end}, 119 | 120 | -- 121 | -- AltHue 122 | -- 123 | 124 | althue = { 125 | panel = function (devNo) 126 | local v = luup.variable_get (sid.althue, "Version", devNo) 127 | return h.span (v or '') 128 | end}, 129 | 130 | -- 131 | -- AltUI 132 | -- 133 | altui = { 134 | panel = function (devNo) 135 | local v = luup.variable_get (sid.altui, "Version", devNo) 136 | return h.span (v or '') 137 | end}, 138 | 139 | -- 140 | -- Camera 141 | -- 142 | DigitalSecurityCamera = { 143 | control = function (devNo) 144 | local ip = luup.attr_get ("ip", devNo) or '' 145 | local stream = luup.variable_get (sid.camera, "DirectStreamingURL", devNo) or '' 146 | local src = table.concat {"http://", ip, stream} 147 | return div {class = "w3-panel", h.iframe {src = src, width="300", height="200"}} 148 | end}, 149 | 150 | -- 151 | -- Motion Sensor 152 | -- 153 | 154 | -- MotionSensor = { 155 | -- panel = function (devNo) 156 | -- local time = luup.variable_get (sid.security, "LastTrip", devNo) 157 | -- return div {class = "w3-tiny w3-display-bottomright", time and todate(time) or ''} 158 | -- end}, 159 | 160 | -- 161 | -- Netatmo 162 | -- 163 | 164 | Netatmo = { 165 | control = function () 166 | local br = h.br{} 167 | return div { 168 | p "Grant plugin access to your weather station data:", 169 | h.form { 170 | action="/data_request", 171 | method="get", 172 | target="_blank", 173 | h.input {type="hidden", name="id", value="lr_Netatmo"}, 174 | h.input {type="hidden", name="page", value="authorize"}, 175 | h.input {type="submit", value="Authorize"} 176 | }, 177 | p "Links to reports:", 178 | p {class="w3-text-indigo", 179 | h.a {href="/data_request?id=lr_Netatmo&page=organization", target="_blank", "Device Tree"}, br, 180 | h.a {href="/data_request?id=lr_Netatmo&page=list", target="_blank", "Device List"}, br, 181 | h.a {href="/data_request?id=lr_Netatmo&page=diagnostics", target="_blank", "Diagnostics"}, br, 182 | }} 183 | end}, 184 | 185 | -- 186 | -- Power Meter 187 | -- 188 | 189 | PowerMeter = { 190 | panel = function (devNo) 191 | local watts = luup.variable_get (sid.energy, "Watts", devNo) 192 | local time = luup.variable_get (sid.energy, "KWHReading", devNo) 193 | local kwh = luup.variable_get (sid.energy, "KWH", devNo) 194 | return h.span {watts or '???', " Watts", h.br(), ("%0.0f"): format(kwh or 0), " kWh", h.br(), 195 | tiny_date_time (time)} 196 | end}, 197 | 198 | -- 199 | -- SceneController 200 | -- 201 | 202 | SceneController = { 203 | panel = function (devNo) 204 | local time = luup.variable_get (sid.scene, "LastSceneTime", devNo) 205 | return tiny_date_time (time) 206 | end, 207 | control = function (devNo) 208 | local isShelly = luup.devices[devNo].id: match "^shelly" 209 | return isShelly and ShellyHomePage (devNo) or "

Scene Controller

" 210 | end}, 211 | -- 212 | -- RGB(W) controller 213 | -- 214 | 215 | DimmableRGBLight = { 216 | control = function (devNo) 217 | local isShelly = luup.devices[devNo].id: match "^shelly" 218 | return isShelly and ShellyHomePage (devNo) or "

Scene Controller

" 219 | end}, 220 | 221 | -- 222 | -- Shellies 223 | -- 224 | GenericShellyDevice = { 225 | control = ShellyHomePage, 226 | }, 227 | 228 | ShellyBridge = { 229 | control = function(bridge) 230 | local tbl = h.table {class = "w3-small"} 231 | tbl.header {"dev #", "name", '', "firmware", "update"} 232 | local shelly = {} 233 | local devs = luup.devices 234 | for i,d in pairs (devs) do 235 | if d.device_num_parent == bridge then 236 | shelly[#shelly+1] = i 237 | end 238 | end 239 | table.sort(shelly) 240 | for _,i in ipairs (shelly) do 241 | local d = devs[i] 242 | local attr = d.attributes 243 | tbl.row {i,d.description, 244 | link("?page=control&device=" .. i), 245 | attr.firmware, attr.firmware_update} 246 | end 247 | return div {p "Shelly devices", tbl} 248 | end}, 249 | 250 | -- 251 | -- Zigbee2MQTT 252 | -- 253 | Zigbee2MQTTBridge = { 254 | panel = function (devNo) 255 | local D = API[devNo] 256 | local time = D.hadevice.LastUpdate 257 | local version = D.attr.version 258 | return h.span {version, h.br(), tiny_date_time (time)} 259 | end, 260 | }, 261 | 262 | -- 263 | -- Generic Sensor 264 | -- 265 | GenericSensor = { 266 | panel = function (devNo) 267 | local v = luup.variable_get (sid.generic, "CurrentLevel", devNo) 268 | return h.span (v or '') 269 | end, 270 | }, 271 | -- 272 | -- Weather (NB. this is the device type for the DarkSkyWeather plugin) 273 | -- 274 | 275 | Weather = { 276 | control = function (devNo) 277 | local class = "w3-card w3-margin w3-round w3-padding" 278 | 279 | local function items (list, prefix) 280 | prefix = prefix or '' 281 | local t = h.table {class="w3-small"} 282 | for _, name in ipairs (list) do 283 | local v = luup.variable_get (sid.weather, prefix..name, devNo) 284 | t.row {name:gsub ("(%w)([A-Z])", "%1 %2"), v} 285 | end 286 | return div {class = "w3-container", t} 287 | end 288 | 289 | local conditions = items {"CurrentConditions", "TodayConditions", "TomorrowConditions", "WeekConditions"} 290 | local current = items ({"Temperature", "Humidity", "DewPoint", "PrecipIntensity", "WindSpeed", 291 | "PrecipType", "PrecipProbability", "WindBearing", "Ozone", "ApparentTemperature", "CloudCover"}, "Current") 292 | local today = items ({"LowTemp", "HighTemp", "Pressure"}, "Today") 293 | local tomorrow = items ({"LowTemp", "HighTemp", "Pressure"}, "Tomorrow") 294 | local time = luup.variable_get (sid.weather, "LastUpdate", devNo) 295 | 296 | return 297 | div { class = "w3-row", 298 | div {class = class, h.h5 "General Conditions", conditions}, 299 | div {class = "w3-half", 300 | div {class = class, h.h5 (time and os.date ("At %H:%M, %d-%b-'%y", time) or '?'), current} }, 301 | div {class = "w3-row w3-half", 302 | div {class = class, h.h5 "Today", today}, 303 | div {class = class, h.h5 "Tomorrow", tomorrow} } } 304 | end}, 305 | 306 | -- 307 | -- ZWay 308 | -- 309 | ZWay = { 310 | control = function() return 311 | div { 312 | h.a {class="w3-text-blue", href="/cgi/zway_cgi.lua", target="_blank", 313 | "Configure ZWay child devices"} } 314 | end}, 315 | 316 | 317 | } 318 | 319 | -- 320 | -- aliases 321 | -- 322 | 323 | panels.DoorSensor = panels.MotionSensor 324 | 325 | 326 | return { 327 | device_panel = panels, 328 | } 329 | 330 | -------------------------------------------------------------------------------- /openLuup/server.lua: -------------------------------------------------------------------------------- 1 | local ABOUT = { 2 | NAME = "openLuup.server", 3 | VERSION = "2024.01.03", 4 | DESCRIPTION = "HTTP/HTTPS GET/POST requests server", 5 | AUTHOR = "@akbooer", 6 | COPYRIGHT = "(c) 2013-2022 AKBooer", 7 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 8 | DEBUG = false, 9 | LICENSE = [[ 10 | Copyright 2013-2022 AK Booer 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | ]] 24 | } 25 | 26 | -- 27 | -- openLuup SERVER - HTTP GET/POST request server 28 | -- 29 | 30 | --[[ 31 | 32 | This HTTP server has gone through many evolutions since ~2013. Although it might seem preferable to 33 | use a native system browser, this turns out to be hard to configure in all the possible systems 34 | on which openLuup might be run. This bespoke server code is adequate (just about.) 35 | 36 | Many people have contributed to finding and fixing bugs over the years, in particular thanks go to: 37 | @amg0, @cybrmage, @d55m14, @explorer (many times), @jswim788, @reneboer, and @vosmont 38 | 39 | --]] 40 | 41 | -- 2019.08.01 Significant refactoring 42 | -- WGET split out into separate openluup.client 43 | -- Servlets now use WSAPI environment as their only input parameter 44 | -- (and return the usual status, headers, iterator parameters) 45 | -- 2019.11.29 add client socket to servlet.execute() parameter list 46 | -- see: https://community.getvera.com/t/expose-http-client-sockets-to-luup-plugins-requests-lua-namespace/211263 47 | 48 | -- 2020.03.20 make module fully reentrant to allow multiple servers (on different ports) 49 | 50 | -- 2022.08.14 fix error message for unknown request type (thanks @Donato) 51 | 52 | -- 2024.01.03 use new tcp module for .server.new() 53 | 54 | 55 | local url = require "socket.url" 56 | 57 | local logs = require "openLuup.logs" 58 | local tcp = require "openLuup.tcp" -- 2024.01.03 for core server functions 59 | local tables = require "openLuup.servertables" -- mimetypes and status_codes 60 | local servlet = require "openLuup.servlet" 61 | local scheduler = require "openLuup.scheduler" -- just for logging servlet jobs (with execution time) 62 | local wsapi = require "openLuup.wsapi" -- to build WSAPI request environment 63 | 64 | --local _log, _debug = logs.register (ABOUT) 65 | local _log = logs.register (ABOUT) 66 | 67 | -- CONFIGURATION DEFAULTS 68 | 69 | local CHUNKED_LENGTH = 16000 -- size of chunked transfers 70 | local MAX_HEADER_LINES = 100 -- limit lines to help mitigate DOS attack or other client errors 71 | 72 | -- TABLES 73 | 74 | local status_codes = tables.status_codes 75 | 76 | local iprequests = {} -- log of incoming requests for console Server page 77 | 78 | local myIP = tables.myIP 79 | 80 | -- return HTML for error given numeric status code and optional extended error message 81 | local function error_html(status, msg) 82 | local html = [[ 83 | 84 | 85 | %d - %s 86 |

%s

87 | 88 | ]] 89 | local title = status_codes[status] or "Error" 90 | local body = msg and tostring(msg) or "Unknown error" 91 | local content = html: format (status, title, body) 92 | return content, "text/html" 93 | end 94 | 95 | 96 | -- local functions 97 | 98 | -- turn an iterator into a single content string 99 | local function make_content (iterator) 100 | local content = {} 101 | for x in iterator do content[#content+1] = tostring(x) end 102 | return table.concat (content) 103 | end 104 | 105 | -- convert individual header names to CamelCaps, for consistency 106 | local function CamelCaps (text) 107 | return text: gsub ("(%a)(%a*)", function (a,b) return a: upper() .. (b or ''): lower() end) 108 | end 109 | 110 | 111 | ---------------------------------------------------- 112 | -- 113 | -- RESPOND to requests over HTTP 114 | -- 115 | 116 | -- generate response from the three WSAPI-style parameters 117 | local function http_response (status, headers, iterator) 118 | 119 | local Hdrs = {} -- force CamelCaps-style header names 120 | for a,b in pairs (headers or {}) do Hdrs[CamelCaps(a)] = b end 121 | headers = Hdrs 122 | 123 | -- 2018.07.06 catch any error in servlet response iterator 124 | 125 | local ok, response = pcall (make_content, iterator) -- just for the moment, simply unwrap the iterator 126 | local content_type = headers["Content-Type"] 127 | local content_length = headers["Content-Length"] 128 | 129 | if not ok then status = 500 end -- 2018.06.07 Internal Server Error 130 | 131 | if status ~= 200 then 132 | headers = {} 133 | response, content_type = error_html (status, response) 134 | content_length = #response 135 | end 136 | 137 | -- see https://mimesniff.spec.whatwg.org/ 138 | if not content_type or content_type == '' then -- limited mimetype sniffing 139 | if response then 140 | local start = response: sub (1,50) : lower () 141 | if start: match "^%s*]" 142 | or start: match "^%s*]" 143 | then content_type = "text/html" 144 | elseif 145 | start: match "^%s*<%?xml" 146 | then content_type = "text/xml" 147 | else 148 | content_type = "text/plain" 149 | end 150 | end 151 | end 152 | 153 | headers["Content-Type"] = content_type 154 | headers["Content-Length"] = content_length 155 | headers["Server"] = "openLuup/" .. ABOUT.VERSION 156 | headers["Access-Control-Allow-Origin"] = "*" 157 | headers["Connection"] = "keep-alive" 158 | 159 | local chunked 160 | if not content_length then 161 | headers["Transfer-Encoding"] = "Chunked" 162 | chunked = true 163 | end 164 | 165 | local crlf = "\r\n" 166 | local status_line = "HTTP/1.1 %d %s" 167 | local h = { status_line: format (status, status_codes[status] or "Unknown error") } 168 | for k, v in pairs(headers) do 169 | if type (v) ~= "table" then v = {v} end -- 2019.07.19 WSAPI sends multiple cookies as an array ??? 170 | for i = 1,#v do h[#h+1] = table.concat { k, ": ", v[i] } end 171 | end 172 | h[#h+1] = crlf -- add final blank line delimiting end of headers 173 | headers = table.concat (h, crlf) 174 | 175 | return headers, response, chunked 176 | end 177 | 178 | -- simple send 179 | local function send (sock, data, ...) 180 | local ok, err, n = sock: send (data, ...) 181 | if not ok then 182 | _log (("error '%s' sending %d bytes to %s"): format (err or "unknown", #data, tostring (sock))) 183 | end 184 | if n then 185 | _log (("...only %d bytes sent"): format (n)) 186 | end 187 | return ok, err, 0 -- 2018.02.07 add 0 chunks! 188 | end 189 | 190 | -- specific encoding for chunked messages (trying to avoid long string problem) 191 | local function send_chunked (sock, x) 192 | local N = #x 193 | local ok, err = true 194 | local i,j = 1, math.min(CHUNKED_LENGTH, N) 195 | local hex = "%x\r\n" 196 | local Nc = 0 197 | while i <= N and ok do 198 | Nc = Nc + 1 199 | send (sock, hex: format (j-i+1)) 200 | ok, err = send (sock,x,i,j) 201 | send (sock, "\r\n") 202 | i,j = j + 1, math.min (j + CHUNKED_LENGTH, N) 203 | end 204 | send (sock, "0\r\n\r\n") 205 | return ok, err, Nc 206 | end 207 | 208 | 209 | -- convert headers to table with name/value pairs, and CamelCaps-style names 210 | local function http_read_headers (sock) 211 | local n = 0 212 | local line, err 213 | local headers = {} 214 | -- TODO: remove quotes, if present, from header values? 215 | local header_format = "(%a[%w%-]*)%s*%:%s*(.+)%s*" -- essentially, header:value pairs 216 | repeat 217 | n = n + 1 218 | line, err = sock:receive() 219 | local hdr, val = (line or ''): match (header_format) 220 | if val then headers[CamelCaps (hdr)] = val end 221 | until (not line) or (line == '') or n > MAX_HEADER_LINES 222 | return headers, err 223 | end 224 | 225 | -- receive client request 226 | local function receive (client) 227 | local wsapi_env -- the request object 228 | local headers, post_content 229 | 230 | local line, err = client:receive() -- read the request line 231 | if err then 232 | client: close (ABOUT.NAME .. ".receive " .. err) 233 | return nil, err 234 | end 235 | 236 | _log (line .. ' ' .. tostring(client)) 237 | 238 | -- Request-Line = Method SP Request-URI SP HTTP-Version CRLF 239 | local method, request_URI, http_version = line: match "^(%u+)%s+(.-)%s+(HTTP/%d%.%d)%s*$" 240 | 241 | if not (method == "GET" or method == "POST") then 242 | err = "Unsupported HTTP request:" .. (method or 'unknown type') 243 | return nil, err 244 | end 245 | 246 | headers, err = http_read_headers (client) 247 | if method == "POST" then 248 | local length = tonumber(headers["Content-Length"]) or 0 249 | post_content, err = client:receive(length) 250 | end 251 | 252 | local URL = url.parse (request_URI) 253 | URL.path = URL.path:gsub ("/port_3480", '') -- 2016.09.16, thanks @explorer, and 2019.08.11 @DesT! 254 | wsapi_env = wsapi.make_env (URL.path, URL.query, headers, post_content, method, http_version) 255 | 256 | return wsapi_env 257 | end 258 | 259 | 260 | --------- 261 | -- 262 | -- this is called by a job for each new client socket connection... 263 | -- may handle multiple requests sequentially through repeated calls to incoming() 264 | -- 265 | local function HTTPservlet (client) 266 | -- incoming() is called by the io.server for each new client request 267 | return function --[[incoming--]] () 268 | local wsapi_env, err = receive (client) -- get the request (in the form of a WSAPI environment) 269 | local request_start = scheduler.timenow() 270 | 271 | -- build response and send it 272 | local function respond (...) 273 | if client.closed then return end -- 2018.04.12 don't bother to try and respond to closed socket! 274 | 275 | local headers, response, chunked = http_response (...) 276 | send (client, headers) 277 | 278 | local send_mode = chunked and send_chunked or send 279 | local ok, err, nc = send_mode (client, response) 280 | local _,_ = ok, err -- TODO: change error handling in respond() 281 | local t = math.floor (1000*(scheduler.timenow() - request_start)) 282 | local completed = "request completed (%d bytes, %d chunks, %d ms) %s" 283 | _log (completed:format (#response, nc, t, tostring(client))) 284 | end 285 | 286 | -- error return 287 | if err then 288 | client: close (ABOUT.NAME.. ".incoming " .. err) 289 | return 290 | end 291 | 292 | -- run the appropriate servlet 293 | -- 2019.11.29 added client parameter 294 | local _, msg, jobNo = servlet.execute (wsapi_env, respond, client) -- returns are as for scheduler.run_job () 295 | 296 | -- log the outcome 297 | if jobNo and scheduler.job_list[jobNo] then 298 | local info = "request: HTTP %s from %s %s" -- 2019.05.11 299 | scheduler.job_list[jobNo].type = 300 | info: format (wsapi_env.REQUEST_METHOD, tostring(client.ip), tostring(client)) 301 | else 302 | _log (msg or "unknown error scheduling servlet") 303 | end 304 | end 305 | end 306 | 307 | ---- 308 | -- 309 | -- start (), sets up the HTTP request handler 310 | -- returns list of utility function(s) 311 | -- 312 | local function start (config) 313 | local port = tostring(config.Port or 3480) 314 | 315 | -- start(), create HTTP server 316 | return tcp.server.new { 317 | port = port, -- incoming port 318 | name = "HTTP:" .. port, -- server name 319 | backlog = config.Backlog or 2000, -- queue length 320 | idletime = config.CloseIdleSocketAfter or 90, -- connect timeout 321 | servlet = HTTPservlet, -- our own servlet 322 | connects = iprequests, -- use our own table for info 323 | sendwait = config.SelectWait or 1.0, -- socket.select() timeout 324 | } 325 | 326 | end 327 | 328 | --------------------------------------------- 329 | 330 | --- return module variables and methods 331 | return { 332 | ABOUT = ABOUT, 333 | 334 | TEST = { -- for testing only 335 | CamelCaps = CamelCaps, 336 | http_response = http_response, 337 | make_content = make_content, 338 | }, 339 | 340 | -- constants 341 | myIP = myIP, 342 | 343 | -- variables 344 | iprequests = iprequests, 345 | 346 | http_handler = servlet.http_handler, -- export for use by console server page 347 | file_handler = servlet.file_handler, 348 | cgi_handler = servlet.cgi_handler, 349 | 350 | --methods 351 | add_callback_handlers = servlet.add_callback_handlers, 352 | start = start, 353 | } 354 | 355 | ----- 356 | -------------------------------------------------------------------------------- /openLuup/sysinfo.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | 6 | ABOUT = { 7 | NAME = "sysinfo.sh", 8 | VERSION = "2018.07.28", 9 | DESCRIPTION = "sysinfo script /etc/cmh-ludl/cgi-bin/cmh/sysinfo.sh", 10 | AUTHOR = "@akbooer", 11 | COPYRIGHT = "(c) 2013-2016 AKBooer", 12 | DOCUMENTATION = "https://github.com/akbooer/openLuup/tree/master/Documentation", 13 | LICENSE = [[ 14 | Copyright 2016 AK Booer 15 | 16 | Licensed under the Apache License, Version 2.0 (the "License"); 17 | you may not use this file except in compliance with the License. 18 | You may obtain a copy of the License at 19 | 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | 22 | Unless required by applicable law or agreed to in writing, software 23 | distributed under the License is distributed on an "AS IS" BASIS, 24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | See the License for the specific language governing permissions and 26 | limitations under the License. 27 | ]] 28 | } 29 | 30 | -- 2016.05.09 original version 31 | -- 2018.07.28 updated to use wsapi.response library 32 | 33 | 34 | local json = require "openLuup.json" 35 | local userdata = require "openLuup.userdata" 36 | local wsapi = require "openLuup.wsapi" 37 | 38 | 39 | local attr = userdata.attributes 40 | 41 | local original_MiOS_shell_script_returns = -- using this, can easily insert new data 42 | { 43 | ["3g_wan_failover"] = "0", 44 | Server_Account = "vera-us-oem-account12.mios.com", 45 | Server_Autha = "vera-us-oem-autha11.mios.com", 46 | Server_Authd = "vera-us-oem-authd11.mios.com", 47 | Server_Device = "vera-us-oem-device12.mios.com", 48 | Server_Event = "vera-us-oem-event12.mios.com", 49 | Server_Firmware = "vera-us-oem-firmware12.mios.com", 50 | Server_Log = "vera-us-oem-log12.mios.com", 51 | Server_Relay = "vera-eu-oem-relay12.mios.com", 52 | Server_Storage = "vera-us-oem-storage12.mios.com", 53 | Server_Support = "vera-us-oem-ts12.mios.com", 54 | account = "123456", 55 | auth_user = "", 56 | ergykey = "", 57 | failsafe_tunnels = "0", 58 | firmware_version = "1.7.0", 59 | full_platform = "mt7620a_Luup_ui7", 60 | hwaddr = "aa:bb:cc:dd:ee:ff", 61 | installation_number = attr.PK_AccessPoint or "87654321", 62 | language = "1", 63 | manual_version = "1", 64 | platform = attr.model or "4Lite", -- was "4Lite" 65 | radisabled = "", 66 | raemail = "", 67 | rapass = "", 68 | raport = "12345", 69 | rauser = "", 70 | remote_only = "1", 71 | secure_unit = "0", 72 | skin = "AltUI" or "mios", -- was "mios" 73 | terminal_disabled = "0", 74 | timezone = "Europe|London|GMT0BST,M3.5.0/1,M10.5.0", 75 | ui_language = "en", 76 | zwave_homeid = "123456768", 77 | zwave_locale = "eu", 78 | zwave_version = "4.5" 79 | } 80 | 81 | 82 | -- WSAPI Lua implementation of sysinfo.sh 83 | 84 | local _log -- defined from WSAPI environment as wsapi.error:write(...) in run() method. 85 | 86 | -- global entry point called by WSAPI connector 87 | function run (wsapi_env) 88 | _log = function (...) wsapi_env.error:write(...) end -- set up the log output, note colon syntax 89 | 90 | _log "running sysinfo.sh WSAPI CGI" 91 | 92 | local res = wsapi.response.new () -- use the response library to build the response! 93 | 94 | local j, err = json.encode (original_MiOS_shell_script_returns) 95 | 96 | res:content_type "text/plain" 97 | res: write (j or err) -- return valid JSON, or error message 98 | 99 | return res: finish() 100 | end 101 | 102 | ----- 103 | -------------------------------------------------------------------------------- /openLuup/whisper-edit.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wsapi.cgi 2 | 3 | module(..., package.seeall) 4 | 5 | ABOUT = { 6 | NAME = "whisper-edit", 7 | VERSION = "2021.08.21", 8 | DESCRIPTION = "Whisper database editor script cgi/whisper-edit.lua", 9 | AUTHOR = "@akbooer", 10 | COPYRIGHT = "(c) 2013-2019 AKBooer", 11 | DOCUMENTATION = "", 12 | } 13 | 14 | -- Whisper file editor, using storage finder and WSAPI request and response libraries 15 | 16 | -- 2016.07.06 based on original whisper-editor 17 | -- 2019.06.07 use w3.css style sheets 18 | -- 2019.06.29 use xhtml module 19 | -- 2019.07.15 use new xml.createHTMLDocument() factory method 20 | 21 | -- 2021.08.21 override invalid end date 22 | 23 | 24 | local whisper = require "openLuup.whisper" 25 | local wsapi = require "openLuup.wsapi" -- for request library 26 | local xml = require "openLuup.xml" 27 | 28 | 29 | local _log -- defined from WSAPI environment as wsapi.error:write(...) in run() method. 30 | 31 | -- global entry point called by WSAPI connector 32 | 33 | 34 | local pagename = "w-edit" 35 | 36 | ----------------------------------- 37 | 38 | local button_class = "w3-button w3-border w3-margin w3-round-large " 39 | 40 | local function ymd (date, hour, min,sec) 41 | local y,m,d = (date or ''): match "(%d%d%d%d)%D(%d%d)%D(%d%d)" 42 | if y then 43 | return os.time {year=y, month=m, day=d, hour=hour, min=min, sec=sec} 44 | end 45 | end 46 | 47 | 48 | ----------------------------------- 49 | -- for future use...? 50 | --[[ 51 | 52 | -- find min and max times in tv array (interleaved times and values) 53 | -- note that a time of zero means, in fact, undefined 54 | local function min_max (x) 55 | local min,max = os.time(),x[1] 56 | for i = 1,#x, 2 do 57 | local t = x[i] 58 | if t > 0 then 59 | if t > max then max = t end 60 | if t < min then min = t end 61 | end 62 | end 63 | return min, max 64 | end 65 | 66 | -- gets the timestamp of the oldest and newest datapoints in file 67 | local function earliest_latest (header) 68 | local archives = header.archives 69 | -- search for latest in youngest archive 70 | local youngest = archives[1].readall() 71 | local _, late = min_max (youngest) 72 | -- early = os.time() - header['maxRetention'] -- instead, search for earliest in oldest archive 73 | local oldest = archives[#archives].readall() 74 | local early = min_max (oldest) 75 | if late < early then late = early end 76 | return Interval (early, late) 77 | end 78 | --]] 79 | 80 | ----------------------------------- 81 | 82 | function run (wsapi_env) 83 | 84 | _log = function (...) wsapi_env.error:write(...) end -- set up the log output, note colon syntax 85 | 86 | local req = wsapi.request.new (wsapi_env) -- use request library to get object with useful methods 87 | local res = wsapi.response.new () -- and the response library to build the response! 88 | 89 | 90 | -- read the basic parameters from ETHER the GET or the POST parameters 91 | 92 | local now = os.time() 93 | local date = "%Y-%m-%d" 94 | local params = req.params 95 | local target = params["target"] 96 | local from = params["from"] 97 | local to = params["until"] 98 | from = ymd(from) and from or os.date (date, now) 99 | to = ymd(to) and to or os.date (date, now) 100 | if to < from then to = from end -- 2021.08.21, override invalid end date 101 | 102 | -- get the requested data 103 | 104 | local I, V, T 105 | local tv = whisper.fetch (target, ymd(from, 0,0,0), ymd(to, 23,59,59)) 106 | if tv then 107 | local n = 0 108 | I, V, T = {}, {}, {} -- I is index table 109 | for _, v,t in tv:ipairs () do 110 | if v then -- only show non-nil data 111 | n = n + 1 112 | T[n] = t 113 | V[n] = v 114 | I[tostring(t)] = n -- also index by text time (since post requests come that way) 115 | end 116 | end 117 | end 118 | 119 | -- POST processing: if valid data and updates, then make changes 120 | 121 | if V and req.method == "POST" then 122 | local post = req.POST 123 | local Tedit, Vedit = {}, {} 124 | for t,v in pairs (post) do -- NB: t is a string 125 | local tn = tonumber (t) 126 | local vn = tonumber (v) 127 | local idx = I[t] 128 | if tn and vn and V[idx] ~= vn then -- has been edited 129 | -- print (tn, "old: " .. V[idx], "new: " .. vn) 130 | Tedit[#Tedit+1] = tn 131 | Vedit[#Vedit+1] = vn 132 | V[idx] = vn -- update table with new value 133 | end 134 | end 135 | 136 | whisper.setAggregationMethod (target, post.aggregation, post.xFilesFactor) -- update aggregation 137 | 138 | luup.log ("Graphite Editor - Number of edits: " .. #Vedit) 139 | -- whisper.update_many (path,values,timestamps, now) 140 | local ok = pcall (whisper.update_many, target, Vedit, Tedit, now) 141 | if not ok then luup.log ("Whisper file update failed: " .. target) end 142 | end 143 | 144 | -- 145 | -- build the HTML page 146 | -- 147 | 148 | local h = xml.createHTMLDocument "W-Edit" 149 | 150 | local read_form = h.div {class = "w3-card w3-margin w3-small", 151 | h.div {class = "w3-container w3-grey", 152 | h.h4 {"Database Query"}}, 153 | h.form {class = "w3-container w3-margin-top", 154 | action=req.script_name, 155 | method="get", 156 | h.input {type="hidden", name="page", value=pagename}, 157 | h.label {"target: ", title="full file path"}, 158 | h.input {class = "w3-input", type="text", name="target", value=target}, 159 | h.label {"from: ", title="from start of this day"}, 160 | h.input {class = "w3-input", type="date", name="from", value=from}, 161 | h.label {"until: ", title="until end of this day"}, 162 | h.input {class = "w3-input", type="date", name="until", value=to}, 163 | h.div {class = "w3-right-align", 164 | h.input {class = button_class .. "w3-pale-green", 165 | type="Submit", value="Read", title="get data to edit"} 166 | }, 167 | }, 168 | } 169 | 170 | -- if there is any data, then build an editable table 171 | 172 | local function row (time, value) 173 | return {os.date ("%Y-%m-%d %H:%M:%S", time), h.input {name=time, value=value}} 174 | end 175 | 176 | local data = '' -- default to blank space 177 | if V then 178 | data = h.table {class = "w3-table"} 179 | data: header {"date / time", "value"} 180 | for i,v in ipairs (V) do 181 | data: row (row (T[i], v)) 182 | end 183 | end 184 | 185 | local info = whisper.info (target) 186 | local aggregation = {} 187 | for _, method in ipairs (whisper.aggregationTypeToMethod) do 188 | local checked 189 | if method == info.aggregationMethod then checked = '1' end 190 | aggregation[#aggregation+1] = h.label {method} 191 | aggregation[#aggregation+1] = h.input {type="radio", name="aggregation", value=method, checked=checked} 192 | end 193 | 194 | local xff = ("%0.2f"):format (info.xFilesFactor) 195 | 196 | local write_form = h.div {class = "w3-card w3-margin w3-small", 197 | h.div {class = "w3-container w3-grey", 198 | h.h4 {"Database Update"}}, 199 | h.form {class = "w3-container w3-margin-top", 200 | action=req.script_name, 201 | method="post", 202 | h.input {type="hidden", name = "page", value = pagename}, 203 | h.input {type="hidden", name = "from", value = from}, 204 | h.input {type="hidden", name = "until", value = to}, 205 | h.input {type="hidden", name = "target", value = target}, 206 | h.label {"archives: ",title="sample rate:time span, ..., for each resolution archive"}, 207 | h.input {class = "w3-input", readonly=1, disabled=1, value = tostring(info.retentions)}, 208 | h.label {"aggregation:", title="function for combining samples between archives"}, 209 | h.div {class = "w3-white w3-padding w3-border-bottom", h.div (aggregation) }, 210 | h.label {"xFilesFactor:", title = "xff (0-1) if you don't know what this is, don't change it"}, 211 | h.input {class = "w3-input", name="xFilesFactor", autocomplete="off", value = xff}, 212 | h.div {class = "w3-right-align", 213 | h.input {class = button_class .. "w3-pale-yellow", 214 | type="Reset", title="clear changes to table"}, 215 | h.input {class = button_class .. "w3-pale-red", 216 | type="Submit", value="Commit", title="write changes back to file"}, 217 | }, 218 | h.div {class = "w3-panel w3-border w3-hover-border-red", data}, 219 | }} 220 | 221 | h.body:appendChild { 222 | h.meta {charset="utf-8", name="viewport", content="width=device-width, initial-scale=1"}, 223 | h.link {rel="stylesheet", href="https://www.w3schools.com/w3css/4/w3.css"}, 224 | h.div {class = "w3-panel w3-cell", read_form, write_form}} 225 | 226 | res:write (tostring(h)) 227 | 228 | return res:finish() 229 | end 230 | 231 | ----- 232 | -------------------------------------------------------------------------------- /tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/tests/.DS_Store -------------------------------------------------------------------------------- /tests/data/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akbooer/openLuup/39f012e6879b32e224b9545323cb805ef1ddd36d/tests/data/.DS_Store -------------------------------------------------------------------------------- /tests/test_all.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | multifile = true 4 | 5 | require "tests.test_loader" 6 | require "tests.test_devices" 7 | require "tests.test_gateway" 8 | require "tests.test_luup" 9 | require "tests.test_requests" 10 | require "tests.test_rooms" 11 | require "tests.test_server" 12 | --require "tests.test_scheduler" 13 | require "tests.test_scenes" 14 | require "tests.test_timers" 15 | require "tests.test_logs" 16 | require "tests.test_json" 17 | require "tests.test_xml" 18 | require "tests.test_io" 19 | require "tests.test_userdata" 20 | require "tests.test_chdev" 21 | require "tests.test_vfs" 22 | require "tests.test_compression" 23 | 24 | t.LuaUnit.run "-v" 25 | -------------------------------------------------------------------------------- /tests/test_chdev.lua: -------------------------------------------------------------------------------- 1 | 2 | local t = require "tests.luaunit" 3 | 4 | -- openLuup.chdev TESTS 5 | 6 | luup = luup or {} -- luup is not really there. 7 | 8 | -- 9 | -- DEVICE CREATE 10 | -- 11 | local chdev = require "openLuup.chdev" 12 | 13 | TestChdevDevice = {} -- low-level device tests 14 | 15 | function TestChdevDevice:setUp () 16 | local devType = "urn:schemas-micasaverde-com:device:HomeAutomationGateway:1" 17 | self.devType = devType 18 | self.d0 = chdev.create { 19 | devNo = 0, 20 | device_type = devType, 21 | statevariables = 22 | { 23 | { service = "myServiceId", variable = "Variable", value = "Value" }, 24 | { service = "anotherSvId", variable = "MoreVars", value = "pi" }, 25 | } 26 | } 27 | end 28 | 29 | function TestChdevDevice:tearDown () 30 | self.d0 = nil 31 | end 32 | 33 | function TestChdevDevice:test_create () 34 | t.assertEquals (type (self.d0), "table") 35 | t.assertEquals (self.d0.device_type, self.devType) 36 | local d = self.d0 37 | 38 | -- check the values 39 | t.assertIsNumber (d.category_num) 40 | t.assertIsString (d.description) 41 | t.assertIsNumber (d.device_num_parent) 42 | t.assertIsString (d.device_type) 43 | t.assertIsBoolean (d.embedded) 44 | t.assertIsBoolean (d.hidden) 45 | t.assertIsString (d.id) 46 | t.assertIsBoolean (d.invisible) 47 | t.assertIsString (d.ip) 48 | t.assertIsString (d.mac) 49 | t.assertIsString (d.pass) 50 | t.assertIsNumber (d.room_num) 51 | t.assertIsNumber (d.subcategory_num) 52 | t.assertIsString (d.udn) 53 | t.assertIsString (d.user) 54 | 55 | -- check all the methods are present: 56 | t.assertIsFunction (d.attr_get) 57 | t.assertIsFunction (d.attr_set) 58 | t.assertIsFunction (d.call_action) 59 | t.assertIsFunction (d.is_ready) 60 | t.assertIsFunction (d.supports_service) 61 | t.assertIsFunction (d.variable_set) 62 | t.assertIsFunction (d.variable_get) 63 | t.assertIsFunction (d.version_get) 64 | 65 | -- check the tables 66 | t.assertIsTable (d.attributes) 67 | t.assertIsTable (d.services) 68 | end 69 | 70 | function TestChdevDevice:test_create_with_file () 71 | local x = chdev.create { 72 | devNo = 42, 73 | description = "Test", 74 | upnp_file = "D_VeraBridge.xml", -- this file is preloaded in the vfs cache 75 | }; 76 | t.assertIsTable (x) 77 | t.assertEquals (x.description, "Test") 78 | t.assertEquals (x.category_num, 1) 79 | t.assertEquals (x.device_type, "VeraBridge") 80 | end 81 | 82 | function TestChdevDevice:test_created_get () -- see if the ones defined initially are there 83 | -- "myServiceId,Variable=Value \n anotherSvId,MoreVars=pi" 84 | local a = self.d0:variable_get ("myServiceId", "Variable") 85 | local b = self.d0:variable_get ("anotherSvId", "MoreVars") 86 | t.assertEquals (a.value, "Value") 87 | t.assertEquals (b.value, "pi") 88 | end 89 | 90 | -- 91 | -- ATTRIBUTES 92 | -- 93 | 94 | TestChdevAttributes = {} 95 | 96 | function TestChdevAttributes:setUp () 97 | local devType = "urn:schemas-micasaverde-com:device:HomeAutomationGateway:1" 98 | self.devType = devType 99 | self.d0 = chdev.create { 100 | devNo = 0, 101 | device_type = devType 102 | } 103 | end 104 | 105 | function TestChdevAttributes:tearDown () 106 | self.d0 = nil 107 | end 108 | 109 | function TestChdevAttributes:test_nil_get () 110 | t.assertEquals (type (self.d0), "table") 111 | local a = self.d0:attr_get "foo" 112 | t.assertIsNil (a) 113 | end 114 | 115 | 116 | function TestChdevAttributes:test_set_get () 117 | local val = "42" 118 | local name = "attr1" 119 | self.d0:attr_set (name, val) 120 | local a = self.d0:attr_get (name) 121 | t.assertEquals (a, val) 122 | end 123 | 124 | function TestChdevAttributes:test_multiple_set () 125 | local val1 = "42" 126 | local val2 = "BBB" 127 | local name1 = "attr1" 128 | local name2 = "attr2" 129 | local tab = {[name1] = val1, [name2] = val2} 130 | self.d0:attr_set (tab) 131 | local a1 = self.d0:attr_get (name1) 132 | local a2 = self.d0:attr_get (name2) 133 | t.assertEquals (a1, val1) 134 | t.assertEquals (a2, val2) 135 | end 136 | 137 | 138 | -- 139 | -- VARIABLES 140 | -- 141 | 142 | TestChdevVariables = {} 143 | 144 | function TestChdevVariables:setUp () 145 | local devType = "urn:schemas-micasaverde-com:device:HomeAutomationGateway:1" 146 | self.devType = devType 147 | self.d0 = chdev.create { 148 | devNo = 0, 149 | device_type = devType 150 | } 151 | end 152 | 153 | function TestChdevVariables:tearDown () 154 | self.d0 = nil 155 | end 156 | 157 | function TestChdevVariables:test_nil_get () 158 | local a = self.d0:variable_get ("srv", "name") 159 | t.assertIsNil (a) 160 | end 161 | 162 | function TestChdevVariables:test_set_get () 163 | local val = "42" 164 | local srv = "myService" 165 | local name = "var1" 166 | self.d0:variable_set (srv, name, val) 167 | local a = self.d0:variable_get (srv, name) 168 | t.assertEquals (a.value, val) 169 | t.assertEquals (a.name, name) 170 | t.assertEquals (a.old, "EMPTY") 171 | t.assertEquals (a.srv, srv) 172 | t.assertEquals (a.dev, 0) 173 | t.assertIsNumber (a.version) 174 | t.assertIsNumber (a.time) 175 | end 176 | 177 | 178 | -- 179 | -- OTHER METHODS 180 | -- 181 | 182 | TestChdevOtherMethods = {} 183 | 184 | function TestChdevOtherMethods:setUp () 185 | local devType = "urn:schemas-micasaverde-com:device:HomeAutomationGateway:1" 186 | self.devType = devType 187 | self.d0 = chdev.create { 188 | devNo = 0, 189 | device_type = devType 190 | } 191 | end 192 | 193 | function TestChdevOtherMethods:tearDown () 194 | self.d0 = nil 195 | end 196 | 197 | function TestChdevOtherMethods:test_is_ready () 198 | t.assertTrue (self.d0:is_ready()) 199 | end 200 | 201 | function TestChdevOtherMethods:test_supports_service () 202 | local srv = "aService" 203 | local var = "varname" 204 | self.d0:variable_set (srv, name, val) 205 | t.assertTrue (self.d0:supports_service (srv)) 206 | t.assertFalse (self.d0:supports_service "foo") 207 | end 208 | 209 | function TestChdevOtherMethods:test_version () 210 | local v1 = self.d0:version_get () 211 | t.assertIsNumber (v1) 212 | local val = "42" 213 | local srv = "myService" 214 | local name = "var1" 215 | local var = self.d0:variable_set (srv, name, val) -- change a variable 216 | local v2 = self.d0:version_get () 217 | t.assertTrue (v2 > v1) -- check version number increments 218 | t.assertEquals (var.version, v2) -- and that variable has same version 219 | end 220 | 221 | function TestChdevOtherMethods:test_call_action () 222 | local srv = "testService" 223 | self.d0.services[srv] = { 224 | actions = { 225 | action1 = { 226 | run = function (lul_device, lul_settings) 227 | return true 228 | end, 229 | }, 230 | action2 = { 231 | job = function (lul_device, lul_settings, lul_job) 232 | return 4, 0 -- job done status 233 | end, 234 | }, 235 | } 236 | } 237 | local error, error_msg, jobNo, return_arguments = self.d0:call_action (srv, "action1", {}) 238 | t.assertEquals (error, 0) 239 | t.assertIsNumber (jobNo) 240 | t.assertEquals (jobNo, 0) -- this is a tag, no job 241 | t.assertIsTable (return_arguments) 242 | 243 | error, error_msg, jobNo, return_arguments = self.d0:call_action (srv, "action2", {}) 244 | t.assertEquals (error, 0) 245 | t.assertIsNumber (jobNo) 246 | t.assertNotEquals (jobNo, 0) -- this is a tag, so job number returned 247 | t.assertIsTable (return_arguments) 248 | end 249 | 250 | function TestChdevOtherMethods:test_missing_action () 251 | local result 252 | local function missing () 253 | return { 254 | run = function (lul_device, lul_settings) 255 | result = lul_settings.value 256 | return true 257 | end 258 | } 259 | end 260 | self.d0:action_callback (missing) 261 | local error, error_msg, jobNo, return_arguments = self.d0:call_action ("garp", "foo", {value=12345}) 262 | t.assertEquals (error, 0) 263 | t.assertIsTable (return_arguments) 264 | t.assertEquals (result, 12345) 265 | end 266 | 267 | function TestChdevOtherMethods:test_ () 268 | end 269 | 270 | -------------------- 271 | 272 | if not multifile then t.LuaUnit.run "-v" end 273 | 274 | -------------------- 275 | -------------------------------------------------------------------------------- /tests/test_compression.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- 4 | -- LZAP compression tests 5 | -- @akbooer, May 2016 6 | -- 7 | 8 | local socket = require "socket" 9 | local timer = socket.gettime 10 | 11 | local C = require "openLuup.compression" 12 | local codec = C.codec 13 | 14 | TestCodec = {} 15 | 16 | 17 | function TestCodec:test_simple () 18 | local c = codec.new "abc" 19 | t.assertIsFunction (c.encode) 20 | t.assertIsFunction (c.decode) 21 | t.assertEquals (c.symbols, 3^2) 22 | 23 | c = codec.new "0123456789" -- '.new' syntax, or... 24 | t.assertEquals (c.symbols, 1e2) 25 | 26 | c = codec () -- ...direct call syntax, either will work 27 | t.assertEquals (c.symbols, 256^2) 28 | end 29 | 30 | function TestCodec:test_json () 31 | local j = codec (codec.json) 32 | t.assertEquals (j.symbols, 92^2) 33 | t.assertEquals (table.concat (j.alphabet), codec.json) -- compare separate byte codes to expected string 34 | local x = {0,1,2,3,4,5} 35 | local y = j.encode (x) 36 | t.assertEquals (y, " ! # $ % & ") -- these are the first codes for this codec 37 | local z = j.decode (y) 38 | t.assertItemsEquals (z, x) 39 | end 40 | 41 | function TestCodec:test_json_header () 42 | local j = codec (codec.json, "HEADER") 43 | t.assertEquals (j.symbols, 92^2) 44 | t.assertEquals (table.concat (j.alphabet), codec.json) -- compare separate byte codes to expected string 45 | local x = {0,1,2,3,4,5} 46 | local y = j.encode (x) 47 | t.assertEquals (y, "HEADER ! # $ % & ") -- the header, then the first codes for this codec 48 | local z = j.decode (y) 49 | t.assertItemsEquals (z, x) 50 | end 51 | 52 | 53 | function TestCodec:test_header () 54 | local j = codec (nil, "TEST Header") 55 | t.assertEquals (j.symbols, 256^2) 56 | local x = {0,1,2,3,4,5} 57 | local y = j.encode (x) 58 | local z = j.decode (y) 59 | t.assertItemsEquals (z, x) 60 | end 61 | 62 | function TestCodec:test_full () 63 | local j = codec () 64 | t.assertEquals (j.symbols, 256^2) 65 | local x = {0,1,2,3,4,5} 66 | local y = j.encode (x) 67 | local z = j.decode (y) 68 | t.assertItemsEquals (z, x) 69 | end 70 | 71 | function TestCodec:test_full2 () 72 | local j = codec (codec.full) 73 | t.assertEquals (j.symbols, 256^2) 74 | local x = {0,1,2,3,4,5} 75 | local y = j.encode (x) 76 | local z = j.decode (y) 77 | t.assertItemsEquals (z, x) 78 | end 79 | 80 | function TestCodec:test_null () 81 | local j = codec (codec.null) 82 | t.assertEquals (j.symbols, 2^53) 83 | local x = {0,1,2,3,4,5} 84 | local y = j.encode (x) 85 | local z = j.decode (y) 86 | t.assertItemsEquals (z, x) 87 | end 88 | 89 | function TestCodec:test_null2 () 90 | local j = codec.null -- alternative way to specify null codec 91 | t.assertEquals (j.symbols, 2^53) 92 | local x = {0,1,2,3,4,5} 93 | local y = j.encode (x) 94 | local z = j.decode (y) 95 | t.assertItemsEquals (z, x) 96 | end 97 | 98 | 99 | TestLZAP = {} 100 | 101 | function TestLZAP:test_simple () 102 | local lzap = C.lzap 103 | local text = [[The rain in Spain stays mainly in the plain]] 104 | local a = lzap.encode (text) -- no codes, so returns array of codewords 105 | t.assertIsTable (a) 106 | local b = lzap.decode (a) 107 | t.assertEquals (b, text) 108 | end 109 | 110 | function TestLZAP:test_yabba () 111 | local lzap = C.lzap 112 | local text = [[yabbadabbadabbadoo]] -- another common test string 113 | local a = lzap.encode (text) -- no codes, so returns array of codewords 114 | t.assertIsTable (a) 115 | local b = lzap.decode (a) 116 | t.assertEquals (b, text) 117 | end 118 | 119 | function TestLZAP:test_unicode () 120 | local lzap = C.lzap 121 | local text = [[Система ß$¢€]] 122 | local a = lzap.encode (text) -- no codes, so returns array of codewords 123 | t.assertIsTable (a) 124 | local b = lzap.decode (a) 125 | t.assertEquals (b, text) 126 | end 127 | 128 | function TestLZAP:test_common_failure () 129 | local lzap = C.lzap 130 | local text = [[aaaaaaaaaaaaaaaaaa]] -- some implementations have a well-known bug 131 | local a = lzap.encode (text) -- no codes, so returns array of codewords 132 | t.assertIsTable (a) 133 | local b = lzap.decode (a) 134 | t.assertEquals (b, text) 135 | end 136 | 137 | 138 | -- encode/decode round trip with 256 code alphabet codec 139 | function TestLZAP:test_with_codec () 140 | local c = codec () -- full-width codec 141 | local lzap = C.lzap 142 | local text = [[sir sid eastman easily teases sea sick seals]] 143 | local a = lzap.encode (text, c) -- codec returns single string of byte-pairs 144 | t.assertIsString (a) 145 | local b = lzap.decode (a, c) 146 | t.assertEquals (b, text) 147 | end 148 | 149 | -- encode/decode round trip with JSON alphabet codec 150 | function TestLZAP:test_with_json_codec () 151 | local text = [[ 152 | The rain it raineth on the just 153 | And also on the unjust fella; 154 | But chiefly on the just, because 155 | The unjust hath the just’s umbrella. 156 | ]] 157 | text = text: rep(250) 158 | local c = codec (codec.json) -- JSON string codec 159 | local lzap = C.lzap 160 | local a = lzap.encode (text, c) -- codec returns single string of byte-pairs 161 | t.assertIsString (a) 162 | local ratio = #text / #a 163 | t.assertTrue (ratio > 25) -- should achieve better than 25:1 compression! 164 | local b = lzap.decode (a, c) 165 | t.assertEquals (b, text) 166 | end 167 | 168 | -- file I/O 169 | 170 | ExtraTests = {} 171 | 172 | -- reads, compresses, and writes file, adding .lzo extension 173 | local function test_file (name, comp, codec, outname) 174 | outname = (outname or name) .. ".lzap" 175 | local f = io.open (name) 176 | if not f then error "can't open file" end 177 | local text = f: read "*a" 178 | f: close() 179 | local t0 = timer () 180 | local coded = comp.encode (text, codec) 181 | local t1 = timer() 182 | local decoded = comp.decode (coded, codec) 183 | local t2 = timer() 184 | t.assertEquals (decoded, text) 185 | local time = "%0.3f, %0.3f (seconds)" 186 | local ratio = "%0.1f" 187 | print ('','['..#text..']',name) 188 | print ('','['..#coded..']', outname) 189 | print ('',"compression ratio: " .. ratio: format (#text / #coded) .. 190 | ", times:"..time:format (t1-t0,t2-t1)) 191 | f = io.open (outname, 'wb') 192 | f: write (coded) 193 | f: close () 194 | end 195 | 196 | 197 | function ExtraTests:test_user_data_json () 198 | local name = "user_data.json" 199 | local comp = C.lzap 200 | local c = codec (codec.json) 201 | test_file (name, comp, c, "tests/data/" .. name ..".JSON") 202 | end 203 | 204 | function ExtraTests:test_user_data_bin () 205 | local name = "user_data.json" 206 | local comp = C.lzap 207 | local c = codec () -- binary codec 208 | test_file (name, comp, c, "tests/data/" .. name ..".BINARY") 209 | end 210 | 211 | 212 | ------------------- 213 | 214 | if multifile then return end 215 | 216 | t.LuaUnit.run "-v" 217 | 218 | -- extra here 219 | print "extra..." 220 | 221 | local N = 0 222 | for a,b in pairs (ExtraTests) do 223 | print (a) 224 | b() 225 | N = N + 1 226 | print "OK\n" 227 | end 228 | 229 | print (N .. " additional tests passed successfully") 230 | 231 | ------------------- 232 | 233 | 234 | ----- 235 | -------------------------------------------------------------------------------- /tests/test_devices.lua: -------------------------------------------------------------------------------- 1 | 2 | local t = require "tests.luaunit" 3 | 4 | -- openLuup.device TESTS 5 | 6 | -- 7 | -- DEVICE CREATE 8 | -- 9 | local d = require "openLuup.devices" 10 | 11 | TestDevice = {} -- low-level device tests 12 | 13 | function TestDevice:setUp () 14 | self.d0 = d.new (0) 15 | self.d0:variable_set ("myServiceId","Variable", "Value") 16 | self.d0:variable_set ("anotherSvId","MoreVars", "pi") 17 | end 18 | 19 | function TestDevice:tearDown () 20 | self.d0 = nil 21 | end 22 | 23 | function TestDevice:test_new () 24 | t.assertEquals (type (self.d0), "table") 25 | local d = self.d0 26 | 27 | -- check all the methods are present: 28 | t.assertIsFunction (d.attr_get) 29 | t.assertIsFunction (d.attr_set) 30 | 31 | t.assertIsFunction (d.action_set) 32 | t.assertIsFunction (d.call_action) 33 | t.assertIsFunction (d.variable_set) 34 | t.assertIsFunction (d.variable_get) 35 | t.assertIsFunction (d.version_get) 36 | 37 | -- check the tables 38 | t.assertIsTable (d.attributes) 39 | t.assertIsTable (d.services) 40 | end 41 | 42 | 43 | function TestDevice:test_created_get () -- see if the ones defined initially are there 44 | local a = self.d0:variable_get ("myServiceId", "Variable") 45 | local b = self.d0:variable_get ("anotherSvId", "MoreVars") 46 | t.assertEquals (a.value, "Value") 47 | t.assertEquals (b.value, "pi") 48 | end 49 | 50 | -- 51 | -- ATTRIBUTES 52 | -- 53 | 54 | TestAttributes = {} 55 | 56 | function TestAttributes:setUp () 57 | local devType = "urn:schemas-micasaverde-com:device:HomeAutomationGateway:1" 58 | self.devType = devType 59 | self.d0 = d.new (0) 60 | end 61 | 62 | function TestAttributes:tearDown () 63 | self.d0 = nil 64 | end 65 | 66 | function TestAttributes:test_nil_get () 67 | t.assertEquals (type (self.d0), "table") 68 | local a = self.d0:attr_get "foo" 69 | t.assertIsNil (a) 70 | end 71 | 72 | 73 | function TestAttributes:test_set_get () 74 | local val = "42" 75 | local name = "attr1" 76 | self.d0:attr_set (name, val) 77 | local a = self.d0:attr_get (name) 78 | t.assertEquals (a, val) 79 | end 80 | 81 | function TestAttributes:test_numeric_set_get () 82 | local val = 1234 83 | local name = "attr1" 84 | self.d0:attr_set (name, val) 85 | local a = self.d0:attr_get (name) 86 | t.assertEquals (type(a), "number") 87 | t.assertEquals (a, val) 88 | end 89 | 90 | function TestAttributes:test_multiple_set () 91 | local val1 = "42" 92 | local val2 = "BBB" 93 | local name1 = "attr1" 94 | local name2 = "attr2" 95 | local tab = {[name1] = val1, [name2] = val2} 96 | self.d0:attr_set (tab) 97 | local a1 = self.d0:attr_get (name1) 98 | local a2 = self.d0:attr_get (name2) 99 | t.assertEquals (a1, val1) 100 | t.assertEquals (a2, val2) 101 | end 102 | 103 | 104 | -- 105 | -- VARIABLES 106 | -- 107 | 108 | TestVariables = {} 109 | 110 | function TestVariables:setUp () 111 | self.d0 = d.new (0) 112 | end 113 | 114 | function TestVariables:tearDown () 115 | self.d0 = nil 116 | end 117 | 118 | function TestVariables:test_nil_get () 119 | local a = self.d0:variable_get ("srv", "name") 120 | t.assertIsNil (a) 121 | end 122 | 123 | function TestVariables:test_set_get () 124 | local val = "42" 125 | local srv = "myService" 126 | local name = "var1" 127 | self.d0:variable_set (srv, name, val) 128 | local a = self.d0:variable_get (srv, name) 129 | t.assertEquals (a.value, val) 130 | t.assertEquals (a.name, name) 131 | t.assertEquals (a.old, "EMPTY") 132 | t.assertEquals (a.srv, srv) 133 | t.assertEquals (a.dev, 0) 134 | t.assertIsNumber (a.version) 135 | t.assertIsNumber (a.time) 136 | end 137 | 138 | 139 | function TestVariables:test_watch () 140 | local val = "42" 141 | local srv = "myService" 142 | local name = "var1" 143 | self.d0:variable_set (srv, name, val) 144 | local v = d.variable_watch (self.d0, my_watch, srv, name) 145 | local a = self.d0:variable_get (srv, name) 146 | t.assertEquals (a.value, val) 147 | t.assertEquals (a.name, name) 148 | t.assertEquals (a.old, "EMPTY") 149 | t.assertNotNil (a.version) 150 | t.assertNotNil (a.time) 151 | local v = d.variable_watch (self.d0, my_watch, srv, "foo") -- wrong variable 152 | t.assertNil (v) 153 | local v = d.variable_watch (self.d0, my_watch, "foo", name) -- wrong service 154 | t.assertNil (v) 155 | end 156 | 157 | function my_watch () 158 | -- won't actually be called since scheduler is not running 159 | end 160 | 161 | -- 162 | -- OTHER METHODS 163 | -- 164 | 165 | TestOtherMethods = {} 166 | 167 | function TestOtherMethods:setUp () 168 | self.d0 = d.new (0) 169 | end 170 | 171 | function TestOtherMethods:tearDown () 172 | self.d0 = nil 173 | end 174 | 175 | function TestOtherMethods:test_version () 176 | local v1 = self.d0:version_get () 177 | t.assertIsNumber (v1) 178 | local val = "42" 179 | local srv = "myService" 180 | local name = "var1" 181 | local var = self.d0:variable_set (srv, name, val) -- change a variable 182 | local v2 = self.d0:version_get () 183 | t.assertTrue (v2 > v1) -- check version number increments 184 | t.assertEquals (var.version, v2) -- and that variable has same version 185 | end 186 | 187 | -- 188 | -- ACTIONS 189 | -- 190 | 191 | TestDeviceActions = {} 192 | 193 | function TestDeviceActions:setUp () 194 | self.d0 = d.new (0) 195 | 196 | -- add an action or two 197 | -- 198 | 199 | local action1 = { 200 | run = function (lul_device, lul_settings) 201 | return true 202 | end, 203 | } 204 | 205 | local action2 = { 206 | job = function (lul_device, lul_settings, lul_job) 207 | return 4, 0 -- job done status 208 | end, 209 | } 210 | 211 | self.d0:action_set ( "testService", "action1", action1) 212 | self.d0:action_set ( "testService", "action2", action2) 213 | 214 | end 215 | 216 | function TestDeviceActions:tearDown () 217 | self.d0 = nil 218 | end 219 | 220 | function TestDeviceActions:test_call_action () 221 | local srv = "testService" 222 | local error, error_msg, jobNo, return_arguments = self.d0:call_action (srv, "action1", {}) 223 | t.assertEquals (error, 0) 224 | t.assertIsNumber (jobNo) 225 | t.assertEquals (jobNo, 0) -- this is a tag, no job 226 | t.assertIsTable (return_arguments) 227 | 228 | error, error_msg, jobNo, return_arguments = self.d0:call_action (srv, "action2", {}) 229 | t.assertEquals (error, 0) 230 | t.assertIsNumber (jobNo) 231 | t.assertNotEquals (jobNo, 0) -- this is a tag, so job number returned 232 | t.assertIsTable (return_arguments) 233 | end 234 | 235 | function TestDeviceActions:test_missing_action_handler () 236 | local result 237 | local function missing () 238 | return { 239 | run = function (lul_device, lul_settings) 240 | result = lul_settings.value 241 | return true 242 | end 243 | } 244 | end 245 | self.d0:action_callback (missing) 246 | local error, error_msg, jobNo, return_arguments = self.d0:call_action ("garp", "foo", {value=12345}) 247 | t.assertEquals (error, 0) 248 | t.assertEquals (error_msg, '') 249 | t.assertEquals (jobNo, 0) 250 | t.assertIsTable (return_arguments) 251 | t.assertEquals (result, 12345) 252 | end 253 | 254 | function TestDeviceActions:test_missing_service () 255 | local e,m,j,a = self.d0:call_action ("foo", "garp", {}, 4321) 256 | t.assertEquals (e, 401) 257 | t.assertEquals (m, "Invalid Service") 258 | t.assertEquals (j, 0) 259 | t.assertIsTable (a) 260 | end 261 | 262 | function TestDeviceActions:test_missing_action () 263 | local e,m,j,a = self.d0:call_action ("testService", "garp", {}, 4321) 264 | t.assertEquals (e, 501) 265 | t.assertEquals (m, "No implementation") 266 | t.assertEquals (j, 0) 267 | t.assertIsTable (a) 268 | end 269 | 270 | -------------------- 271 | 272 | if not multifile then t.LuaUnit.run "-v" end 273 | 274 | -------------------- 275 | -------------------------------------------------------------------------------- /tests/test_gateway.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | Test_gateway = {} 4 | 5 | 6 | ----- 7 | -- 8 | -- TEST gateway 9 | -- 10 | 11 | luup = require "openLuup.luup" 12 | local json = require "openLuup.json" 13 | local g = require "openLuup.gateway" 14 | 15 | local SID = "urn:micasaverde-com:serviceId:HomeAutomationGateway1" 16 | 17 | local params = { 18 | DataFormat = "json", 19 | inUserData = json.encode {StartupCode = "-- this is where the startup code goes\n"} 20 | } 21 | 22 | function Test_gateway:test_startup () 23 | 24 | g.services[SID].actions.ModifyUserData.run ('', params) 25 | 26 | 27 | local startup = luup.attr_get "StartupCode" 28 | t.assertEquals (startup, "-- this is where the startup code goes\n") 29 | 30 | end 31 | 32 | 33 | --------------------- 34 | 35 | if multifile then return end 36 | t.LuaUnit.run "-v" 37 | 38 | --------------------- 39 | 40 | -------------------------------------------------------------------------------- /tests/test_hag.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | Test_hag = {} 4 | 5 | 6 | ----- 7 | -- 8 | -- TEST upnp.control.hag WSAPI CGI 9 | -- 10 | 11 | local luup = require "openLuup.luup" 12 | local hag = require "openLuup.hag" 13 | 14 | local content = [[ 15 | 16 | 17 | 18 | 19 | { 20 | "devices":{}, 21 | "scenes":{}, 22 | "sections":{}, 23 | "rooms":{}, 24 | "InstalledPlugins":[], 25 | "PluginSettings":[], 26 | "users":{}, 27 | "StartupCode": 28 | "-- this is where the startup code goes\n"} 29 | 30 | json 31 | 32 | 33 | 34 | ]] 35 | 36 | function Test_hag:test_startup () 37 | 38 | local status,headers,iterator = hag.run { 39 | error = {write = function (_, ...) print (...) end}, -- TODO: use internal buffer and check contents 40 | input = {read = function () return content end}, 41 | } 42 | 43 | local content = iterator() 44 | t.assertEquals (status, 200) 45 | t.assertIsTable (headers) 46 | t.assertEquals (content, "OK") 47 | 48 | local startup = luup.attr_get "StartupCode" 49 | t.assertEquals (startup, "-- this is where the startup code goes\n") 50 | 51 | end 52 | 53 | 54 | --------------------- 55 | 56 | if multifile then return end 57 | t.LuaUnit.run "-v" 58 | 59 | --------------------- 60 | 61 | -------------------------------------------------------------------------------- /tests/test_io.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup.scenes TESTS 4 | 5 | local io = require "openLuup.io" 6 | 7 | 8 | TestIO = {} 9 | 10 | function TestIO:setUp () 11 | end 12 | 13 | 14 | 15 | function TestIO:test_ () 16 | 17 | end 18 | 19 | --------------------- 20 | 21 | if multifile then return end 22 | t.LuaUnit.run "-v" 23 | 24 | --------------------- 25 | 26 | -------------------------------------------------------------------------------- /tests/test_json.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- JSON module tests 4 | 5 | local msg_check = false 6 | local warning_check = false 7 | local invalid_check = false 8 | 9 | J = require 'openLuup.json' 10 | --J = require 'dkjson' 11 | 12 | --TestDecode = require 'dk-json' 13 | 14 | --require 'cf-json' 15 | --TestDecode = json 16 | -- 17 | --require 'cm-json' 18 | --TestDecode = _G["cm-json"] 19 | 20 | --require 'sb-json' 21 | --TestDecode = {decode = Json.Decode, encode = Json.Encode} 22 | 23 | --require "jf-json" 24 | --TestDecode = JSON 25 | --JSON:decode(raw_json_text) -- all wrong at the moment 26 | 27 | --function J.decode (...) 28 | -- a,b,c,d = pcall (TestDecode.decode, ...) 29 | -- if a then return b,d else return a,b end 30 | --end 31 | -- 32 | --function J.encode (...) 33 | -- a,b,c,d = pcall (TestDecode.encode, ...) 34 | -- if a then return b,d else return a,b end 35 | --end 36 | -- 37 | local N = 0 38 | 39 | --- JSON decode validity tests 40 | 41 | local function invalid (j) 42 | N = N + 1 43 | local lua, msg = J.decode (j) 44 | t.assertIsNil (lua) 45 | if msg_check then t.assertIsString (msg) end 46 | end 47 | 48 | local function warning (j, v) 49 | if not warning_check then return end 50 | N = N + 1 51 | local lua, msg = J.decode (j) 52 | t.assertEquals (lua, v) 53 | if msg_check then t.assertIsString (msg) end 54 | end 55 | 56 | local function valid (j, v) 57 | N = N + 1 58 | local lua, msg = J.decode (j) 59 | t.assertEquals (lua, v) 60 | if msg_check then t.assertIsNil (msg) end 61 | end 62 | 63 | -- INVALID 64 | 65 | DecodeInvalid = {} 66 | 67 | function DecodeInvalid:test_literals () 68 | invalid "foo" 69 | invalid " " 70 | -- invalid {} 71 | -- invalid (false) 72 | -- invalid (42) 73 | end 74 | 75 | function DecodeInvalid:test_numerics () 76 | invalid "E" 77 | invalid "+" 78 | invalid "-" 79 | invalid "+4" 80 | invalid "+-4" 81 | invalid "inf" 82 | end 83 | 84 | function DecodeInvalid:test_strings () 85 | invalid ' "no end in sight' -- unclosed string 86 | invalid ' "also no end \\' -- ditto 87 | invalid ' "wrong = \\udefg"' -- should be numeric code 88 | invalid ' "looks ok but is\'nt \\\" ' 89 | end 90 | 91 | function DecodeInvalid:test_tables () 92 | invalid '[{true}]' 93 | invalid '[1, , 3]' 94 | invalid '[1, "two":2, 3 ] ' 95 | invalid '{"a" :1, 2}' 96 | invalid '[ 42 }' 97 | invalid '{"a": 7]' 98 | invalid '{"a"=7}' 99 | end 100 | 101 | 102 | -- WARNING 103 | 104 | TestDecodeWarning = {} 105 | 106 | function TestDecodeWarning:test_literals () 107 | warning ("true false", true) 108 | end 109 | 110 | function TestDecodeWarning:test_numerics () 111 | warning ("33,4" , 33) 112 | end 113 | 114 | function TestDecodeWarning:test_strings () 115 | warning ('"two" "strings"', "two") 116 | end 117 | 118 | function TestDecodeWarning:test_tables () 119 | warning ('{} {}', {}) 120 | end 121 | 122 | -- VALID 123 | 124 | TestDecodeValid = {} 125 | 126 | function TestDecodeValid:test_literals () 127 | valid ('true', true) 128 | valid ('false', false) 129 | valid ('null', nil) 130 | end 131 | 132 | function TestDecodeValid:test_numerics () 133 | local Inf = 8.88e888 -- my Json's representation of infinity 134 | valid ("0", 0) 135 | valid ("-0", 0) 136 | valid ("42", 42) 137 | valid ("3.14159", 3.14159) 138 | valid ("60328.924", 60328.924) 139 | valid ("-7", -7) 140 | valid ("3e-6", 0.000003) 141 | valid ("2.718E+5", 2.718E+5) 142 | valid ("-1e-999", -0) 143 | valid ("1.0e-789", 0) 144 | valid ("9.99e999", Inf) 145 | valid ("-1.23e+456", -Inf) 146 | end 147 | 148 | function TestDecodeValid:test_strings () 149 | valid ('""', '') 150 | valid ('" " ', ' ') 151 | valid ('"ok string"', 'ok string') 152 | valid ('" also \\" ok"', ' also " ok') 153 | valid ('"Ice\\/Snow"', 'Ice/Snow') 154 | valid ('"Sébastien"', 'Sébastien') 155 | valid ('"a = \\u0061 = \097"', 'a = a = a') 156 | valid ('"\161\162\163"', '\161\162\163') 157 | valid ('"should be ok \\\\"', 'should be ok \\') 158 | valid ('"1234 UTF-8 ß$¢€"', '1234 UTF-8 ß$¢€') 159 | valid ('" \\" \\/ \\! \t "', ' " / ! \t ') 160 | valid ('"\\c\\d\\e...\\x\\y\\z"', 'cde...xyz') 161 | valid ('"quoted solidus \\/ ok?"', 'quoted solidus / ok?') 162 | valid ('"tricky double backslash \\\\\\\\"', 'tricky double backslash \\\\') 163 | valid ('"1234 UTF-8 ß$¢€"', '1234 UTF-8 ß$¢€') 164 | valid ('"Система безопасности и обновлени"', "Система безопасности и обновлени") 165 | end 166 | 167 | function TestDecodeValid:test_tables () 168 | valid ("[]", {}) 169 | valid ("[] ", {}) 170 | valid ("{}", {}) 171 | valid ("{} ", {}) 172 | valid (" [42] ", {42}) 173 | valid ("[true]", {true}) 174 | valid (" [[true]]", {{true}}) 175 | valid ("[1, 2, 3 ] ", {1,2,3}) 176 | valid ('[null, 1, null]', {nil, 1, nil}) 177 | valid ('[{}]', {{}}) 178 | valid ('[[],{}]', {{},{}}) 179 | valid ('{"a" :1, "b" : 2}', {a=1, b=2}) 180 | valid ('{"a":[], "b" : [] } ', {a={}, b={}}) 181 | valid ('{"Ice\\/Snow":"Ice\\/Snow"} ', {["Ice/Snow"] = "Ice/Snow"}) 182 | valid ('["one","two","three","four"] ', {"one","two","three","four"}) 183 | valid ('["one", true, 3, false]', {"one", true, 3, false}) 184 | end 185 | 186 | 187 | --- JSON encode validity tests 188 | 189 | local function invalid (j) 190 | N = N + 1 191 | local lua, msg = J.decode (j) 192 | t.assertIsNil (lua) 193 | if msg_check then t.assertIsString (msg) end 194 | end 195 | 196 | local function valid (lua, v) 197 | N = N + 1 198 | local json, msg = J.encode (lua) 199 | t.assertEquals (json, v) 200 | if msg_check then t.assertIsNil (msg) end 201 | end 202 | 203 | 204 | 205 | EncodeInvalid = {} 206 | 207 | 208 | function EncodeInvalid:test_literals () 209 | invalid (function () end) -- JSON can't serialise functions 210 | end 211 | 212 | function EncodeInvalid:test_tables () 213 | local circular = {} 214 | circular[1] = circular invalid {[0] = 1} 215 | invalid (circular) 216 | invalid {[function () end] = true} 217 | invalid {[1]='a',a=1} 218 | end 219 | 220 | 221 | TestEncodeValid = {} 222 | 223 | 224 | function TestEncodeValid:test_literals () 225 | valid (true, "true") 226 | valid (false, "false") 227 | valid (nil, 'null') 228 | end 229 | 230 | function TestEncodeValid:test_numerics () 231 | local Inf = "8.88e888" 232 | valid (0, "0") 233 | valid (-0, "-0") 234 | valid (1, "1") 235 | valid (-1, "-1") 236 | valid (1.2345, "1.2345") 237 | valid (1.23e45, "1.23e+45") 238 | valid (-33e-33, "-3.3e-32") 239 | -- valid (math.huge, Inf) 240 | -- valid (8.88e888, Inf) 241 | -- valid (-math.huge, '-'..Inf) 242 | valid (0/0, "null") 243 | end 244 | 245 | function TestEncodeValid:test_strings () 246 | valid ("easy", '"easy"') 247 | -- valid ("solidus / ok", '"solidus \\/ ok"') 248 | valid ("double escape \\\\", '"double escape \\\\\\\\"') 249 | valid ("control: \014 14", '"control: \\u000e 14"') 250 | valid ( "1234 UTF-8 ß$¢€", '"1234 UTF-8 ß$¢€"') 251 | valid ("Система безопасности и обновлени", '"Система безопасности и обновлени"') 252 | -- valid ("weird:\\u0000\a\bcde\fghijklm\nopq\rs\tu\vwxy\z\3\15\123 \\ \" \' /\125" , '') 253 | end 254 | 255 | function TestEncodeValid:test_tables () 256 | -- valid ({1, nil, 3}, '[1,null,3]') 257 | -- next is tricky because of sorting and pretty printing 258 | -- valid ({ array = {1,2,3}, string = "is a str", num = 42, boolean = true}, 259 | -- '{"array":[1,2,3],"string":"is a str","num":42,"boolean":true}') 260 | end 261 | 262 | 263 | ------------------- 264 | 265 | if invalid_check then 266 | TestDecodeInvalid = DecodeInvalid 267 | TestEncodeInvalid = EncodeInvalid 268 | end 269 | 270 | 271 | 272 | ------------------- 273 | 274 | if multifile then return else t.LuaUnit.run "-v" end 275 | 276 | print ("TOTAL number of tests run = ", N) 277 | 278 | ------------------- 279 | 280 | print '\nJSON File Tests---------------\n\n' 281 | 282 | 283 | json_files = 284 | { 285 | 'netatmo.json', 286 | 'dataMineConfig.json', 287 | 'user_data.json', 288 | } 289 | 290 | local t0,t1 291 | local de,ee 292 | local lua,json, original 293 | 294 | for _,fn in ipairs (json_files) do 295 | -- local f = io.open ('json/'..fn,'r') 296 | local f = io.open (fn,'r') 297 | if f then 298 | json = f:read ('*a') 299 | f: close () 300 | print ('\n'..fn..': '..#json/1e3 ..' kB') 301 | t0 = os.clock() 302 | lua,de = J.decode(json) 303 | t1 = os.clock() 304 | print ('','decode time = '.. (t1-t0)*1000 ..' mS' ) 305 | print ('','decode status = ', de or 'OK') 306 | t0 = os.clock() 307 | json, ee = J.encode(lua) 308 | t1 = os.clock() 309 | 310 | -- print (json) 311 | 312 | print ('','encode kB = '..#json/1000) 313 | print ('','encode time = '.. (t1-t0)*1000 ..' mS' ) 314 | print ('','encode status = ', ee or 'OK') 315 | if not de or ee then 316 | l2 = J.decode (json) 317 | j2 = J.encode (l2) 318 | if j2 ~= json then print ('round trip encode/decode mismatch ['..#json..' / '..#j2..']') 319 | else print ('round trip encode/decode match OK ['..#json..']') 320 | end 321 | if json == j2 then print 'YES!!!' 322 | end 323 | end 324 | end 325 | end 326 | print '\ndone' 327 | 328 | -------------------- 329 | -------------------------------------------------------------------------------- /tests/test_logs.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup.logs TESTS 4 | 5 | local log = require "openLuup.logs" 6 | 7 | TestLogs = {} 8 | 9 | function TestLogs:setUp () 10 | end 11 | 12 | function TestLogs:test_openLuup_log () 13 | local log = log.openLuup_logger {name = "tests/data/Test.log", versions = 3, lines =20}.send 14 | for i = 1,50 do 15 | log (i) 16 | end 17 | end 18 | 19 | 20 | function TestLogs:test_altui_slog () 21 | local alt = log.altui_logger {name = "tests/data/TestALT.log", lines =20} 22 | local slog = alt.scene 23 | 24 | local scn = {id = 42, name = "foo"} -- log a scene running 25 | local s = slog (scn) 26 | log.altui_scene (scn) -- check it works to the real location too 27 | 28 | local scene = "%d*\t(%d*/%d*/%d*%s%d*:%d*:%d*.%d*).*Scene::RunScene running %d+ (.*) <.*" 29 | local a,b = s: match (scene) 30 | t.assertIsString (a) 31 | t.assertIsString (b) 32 | t.assertEquals (b, "foo") 33 | end 34 | 35 | function TestLogs:test_altui_vlog () 36 | local alt = log.altui_logger {name = "tests/data/TestALT.log", lines =20} 37 | local vlog = alt.variable 38 | 39 | local var = {dev = 42, srv = "myService", name = "foo", old = nil, value = 123, watchers = {1,2,3}} 40 | local v = vlog (var) 41 | log.altui_variable (var) -- check it works to the real location too 42 | 43 | -- first 'grep' pass: 44 | local n = v:match "Device_Variable::m_szValue_set device: 42.*;1m(.+)\027" 45 | t.assertEquals (n, "foo") 46 | 47 | -- second 'JavaScript' pass: 48 | local variable = "%d*\t(%d*/%d*%/%d*%s%d*:%d*:%d*.%d*).*was: (.*) now: (.*) #.*" 49 | local a,b,c = v: match (variable) 50 | t.assertIsString (a) 51 | t.assertIsString (b) 52 | t.assertIsString (c) 53 | t.assertEquals (b, "MISSING") 54 | t.assertEquals (c, "123") 55 | end 56 | 57 | 58 | function TestLogs:test_ () 59 | end 60 | 61 | 62 | --------------------- 63 | 64 | if multifile then return end 65 | t.LuaUnit.run "-v" 66 | 67 | --------------------- 68 | 69 | -------------------------------------------------------------------------------- /tests/test_rooms.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup.logs TESTS 4 | 5 | local luup = require "openLuup.luup" 6 | local room = luup.rooms 7 | 8 | TestRooms = {} 9 | 10 | function TestRooms:setUp () 11 | end 12 | 13 | luup = luup or {rooms = {}, devices = {}, scenes = {}} 14 | 15 | 16 | --Example: http://ip_address:3480/data_request?id=room&action=create&name=Kitchen 17 | function TestRooms:test_room_create () 18 | local name = "A curious room name" 19 | local n = #luup.rooms 20 | local r = room.create (name) 21 | local m = #luup.rooms 22 | t.assertEquals (m, n+1) 23 | t.assertEquals (r, m) 24 | t.assertEquals (luup.rooms[m], name) 25 | end 26 | 27 | --Example: http://ip_address:3480/data_request?id=room&action=rename&room=5&name=Garage 28 | function TestRooms:test_room_rename () 29 | local new_name = "test_room_rename" 30 | local r = room.create "test_room_name" 31 | local n = #luup.rooms 32 | room.rename (r, new_name) 33 | local m = #luup.rooms 34 | t.assertEquals (m, n) 35 | t.assertEquals (luup.rooms[r], new_name) 36 | end 37 | 38 | ----Example: http://ip_address:3480/data_request?id=room&action=delete&room=5 39 | function TestRooms:test_room_delete () 40 | local r = room.create "room name to be deleted" 41 | local n = #luup.rooms 42 | room.delete (r) 43 | local m = #luup.rooms 44 | t.assertEquals (m, n-1) 45 | t.assertIsNil (luup.rooms[n]) 46 | end 47 | 48 | 49 | --------------------- 50 | 51 | if multifile then return end 52 | t.LuaUnit.run "-v" 53 | 54 | --------------------- 55 | 56 | -------------------------------------------------------------------------------- /tests/test_scenes.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup.scenes TESTS 4 | 5 | luup = require "openLuup.luup" 6 | 7 | local l = require "openLuup.loader" 8 | local s = require "openLuup.scenes" 9 | local j = require "openLuup.json" 10 | 11 | 12 | local example_scene = { 13 | id = 1, 14 | name = "Example Scene", 15 | room = 0, 16 | groups = { 17 | { 18 | delay = 0, 19 | actions = { 20 | { 21 | action = "ToggleState", 22 | arguments = { -- NB. these are name/value pairs NOT {name=value, ...} 23 | {name = "param1", value = "42"}, 24 | {name = "param2", value = "77"}, 25 | }, 26 | device = "94", -- why this is a string, I have NO idea 27 | service ="urn:micasaverde-com:serviceId:HaDevice1", 28 | }, 29 | }, 30 | }, 31 | }, 32 | timers = { 33 | { 34 | enabled = 1, 35 | id = 1, 36 | interval = "5m", -- this is actually the "time" parameter in timer calls 37 | name = "Five minutes", 38 | type = 1, 39 | } , 40 | { 41 | enabled = 1, 42 | id = 2, 43 | interval = "10", -- this is actually the "time" parameter in timer calls 44 | name = "Ten Seconds", 45 | type = 1, 46 | } , 47 | }, 48 | lua = -- this HAS to be a string for external tools to work. 49 | [[ 50 | luup.log "hello from Example Scene" 51 | ]], 52 | } 53 | 54 | local json_example = j.encode (example_scene) 55 | 56 | TestScenes = {} 57 | 58 | function TestScenes:setUp () 59 | end 60 | 61 | function TestScenes:test_scene_create () 62 | local sc, err = s.create (example_scene) -- works with Lua or JSON scene definition 63 | t.assertIsNil (err) 64 | t.assertIsTable (sc) 65 | local luup_scene = 66 | { 67 | description="Example Scene", 68 | hidden=false, 69 | page=0, 70 | paused=false, 71 | remote=0, 72 | room_num=0 73 | } 74 | t.assertItemsEquals (sc, luup_scene) 75 | t.assertIsFunction (sc.rename) 76 | t.assertIsFunction (sc.run) 77 | t.assertIsFunction (sc.stop) 78 | t.assertIsFunction (sc.user_table) 79 | -- now trigger it! 80 | local trig = {enabled = 1, name = "test trigger"} 81 | sc.run (trig) 82 | t.assertIsNumber (trig.last_run) -- check that last run time has been inserted 83 | local u = sc: user_table () -- get the user_data version of things 84 | t.assertNotNil (u.last_run) 85 | t.assertEquals (u.last_run, trig.last_run) 86 | end 87 | 88 | function TestScenes:test_scene_stop () 89 | local sc, err = s.create (json_example) 90 | local u = sc: user_table () -- get the user_data version of things 91 | for _,tim in pairs (u.timers) do 92 | t.assertEquals (tim.enabled, 1) -- check all enabled 93 | end 94 | sc:stop() 95 | for _,tim in pairs (u.timers) do 96 | t.assertEquals (tim.enabled, 0) -- check all disabled 97 | end 98 | end 99 | 100 | function TestScenes:test_scene_list () 101 | local sc, err = s.create (json_example) 102 | local list = tostring (sc) 103 | t.assertIsString (list) 104 | end 105 | 106 | function TestScenes:test_scene_rename () 107 | local sc, err = s.create (json_example) 108 | local nn,nr = "New Name", 3 109 | sc.rename (nn, nr) 110 | local u = sc: user_table () -- get the user_data version of things 111 | t.assertEquals (u.name, nn) 112 | t.assertEquals (u.room, nr) 113 | t.assertEquals (sc.description, nn) 114 | t.assertEquals (sc.room_num, nr) 115 | end 116 | 117 | function TestScenes:test_scene_lua () 118 | local lua_scene = { 119 | id = 41, 120 | name = "lua_scene", 121 | lua = [[ 122 | luup.log ("HELLO from " .. _NAME ) 123 | for i in pairs (_G) do luup.log (i) end 124 | ]] 125 | } 126 | local sc, err = s.create (lua_scene) -- works with Lua or JSON scene definition 127 | local u = sc: user_table () -- get the user_data version of things 128 | t.assertIsString (u.lua) 129 | sc.run() 130 | end 131 | 132 | function TestScenes:test_multiple_scene_lua () 133 | local lua_scene_42 = { 134 | id = 42, 135 | name = "lua_scene_multi1", 136 | lua = [[ 137 | scene_global = 42 138 | luup.log "HELLO from 42" 139 | luup.log ("scene global = " .. scene_global) 140 | ]] 141 | } 142 | local lua_scene_43 = { 143 | id = 43, 144 | name = "lua_scene_multi2", 145 | lua = [[ 146 | scene_global = (scene_global or 0) + 1 147 | luup.log "HELLO from 43" 148 | luup.log ("scene global = " .. scene_global) 149 | ]] 150 | } 151 | local sc42,err42 = s.create (lua_scene_42) 152 | local sc43,err43 = s.create (lua_scene_43) -- works with Lua or JSON scene definition 153 | local u42 = sc42: user_table () -- get the user_data version of things 154 | local u43 = sc43: user_table () -- get the user_data version of things 155 | t.assertIsString (u42.lua) 156 | t.assertIsString (u43.lua) 157 | t.assertIsNil (err42) 158 | t.assertIsNil (err43) 159 | sc42.run() 160 | sc43.run() -- check that they talk to one another 161 | t.assertEquals (s.environment.scene_global, 43) 162 | end 163 | 164 | function TestScenes:test_verify () 165 | local lua_scene_123 = { 166 | id = 123, 167 | name = "non_existent_device_scene", 168 | groups = { 169 | [1] = { 170 | actions = { 171 | {device = "123"} 172 | } 173 | } 174 | }, 175 | } 176 | t.assertIsNil (luup.devices[123]) 177 | local sc,err = s.create (lua_scene_123) 178 | t.assertIsNil (err) 179 | local u1 = sc: user_table () -- get the user_data version of things 180 | t.assertEquals (#u1.groups[1].actions, 0) -- check action has been removed 181 | sc.verify() 182 | end 183 | 184 | function TestScenes:test_verify_all () 185 | s.verify_all () 186 | end 187 | 188 | local pretty = require "pretty" 189 | 190 | function TestScenes:test_room_rename () 191 | local scene = {} 192 | for a,b in pairs (example_scene) do -- make local copy of template 193 | scene[a] = b 194 | end 195 | local roomNo = luup.rooms.create "TestScenesRoom" 196 | scene.room = roomNo 197 | scene.name = "TestScenes:test_room" 198 | local sc, err = s.create (scene) -- works with Lua or JSON scene definition 199 | -- print(pretty(luup.scenes)) 200 | t.assertIsNil (err) 201 | t.assertEquals (sc.room_num, roomNo) 202 | luup.rooms.delete (roomNo) 203 | print (sc) 204 | -- t.assertEquals (sc.room_num, 0) 205 | end 206 | 207 | function TestScenes:test_ () 208 | 209 | end 210 | 211 | --------------------- 212 | 213 | if multifile then return end 214 | t.LuaUnit.run "-v" 215 | 216 | --------------------- 217 | local pretty = require "pretty" 218 | 219 | local sc, err = s.create (json_example) 220 | 221 | print (pretty(sc)) 222 | print "---------------" 223 | 224 | print (tostring(sc)) 225 | 226 | 227 | ----- 228 | 229 | -------------------------------------------------------------------------------- /tests/test_scheduler.lua: -------------------------------------------------------------------------------- 1 | 2 | local t = require "tests.luaunit" 3 | 4 | -- openLuup.scheduler TESTS 5 | 6 | local socket = require "socket" -- for delay function 7 | local s = require "openLuup.scheduler" 8 | local json = require "openLuup.json" 9 | 10 | luup = {} -- for device context 11 | 12 | local TIMEOUT = 1 13 | local jobReturn 14 | 15 | -- THIS IS WHAT A CLIENT JOB LOOKS LIKE: 16 | 17 | local N = 0 18 | local sequence = {s.state.Requeue, s.state.InProgress, s.state.Done} 19 | 20 | local myJob = { 21 | 22 | -- (not really a job) 23 | -- variables: lul_device is a number that is the device id. lul_settings is a table with all the arguments to the action. 24 | -- return value: true or false where true means the function ran ok, false means it failed. 25 | run = function (lul_device, lul_settings) 26 | jobReturn = {device = lul_device, settings=lul_settings, comment = " tag"} -- save our environment 27 | return true 28 | end, 29 | 30 | -- 31 | -- variables: lul_device is a number that is the device id. lul_settings is a table with all the arguments to the action. lul_job is the id number of the job. 32 | -- return value: The first is the job status and is a number from 0-5, and the second is the timeout in seconds. 33 | job = function (lul_device, lul_settings, lul_job) 34 | N = N + 1 35 | return sequence[N] -- run through the test sequence 36 | end, 37 | 38 | -- 39 | -- variables: same as for job above. 40 | -- return value: same as for job above 41 | timeout = function (lul_device, lul_settings, lul_job) 42 | return job_state.Done 43 | end, 44 | 45 | -- (returned by a job) 46 | -- variables: same as for job above, plus lul_data which is a binary string with the data received 47 | -- return value: return 3 values with the syntax return a,b,c. 48 | -- The first two are the same as with job, and the 3rd is a true or false indicating if the incoming data was intended for this job. 49 | incoming = function (lul_device, lul_settings, lul_job, lul_data) 50 | end, 51 | 52 | } -- END OF myJob 53 | 54 | 55 | --------------------------------------- 56 | 57 | 58 | TestScheduler = {} -- luup tests 59 | 60 | function TestScheduler:setUp () 61 | end 62 | 63 | function TestScheduler:tearDown () 64 | end 65 | 66 | -- basics 67 | 68 | function TestScheduler:test_basic_types () 69 | t.assertIsFunction (s.device_start) 70 | t.assertIsFunction (s.run_job) 71 | -- t.assertIsFunction (s.job_watch) 72 | -- t.assertIsFunction (s.get) 73 | t.assertIsFunction (s.status) 74 | t.assertIsFunction (s.sleep) 75 | t.assertIsFunction (s.timenow) 76 | -- t.assertIsFunction (s.set) 77 | end 78 | 79 | function TestScheduler:test_sleep () 80 | s.sleep (2000) -- two seconds delay 81 | end 82 | 83 | function TestScheduler:test_status () 84 | local status, notes = s.status (42) -- missing job 85 | t.assertEquals (status, s.state.NoJob) 86 | t.assertIsString (notes) 87 | t.assertEquals (notes , "no such job #42") 88 | end 89 | 90 | function TestScheduler:test_run_true () 91 | local runTrue = { run = function () return true end} 92 | local error, error_msg, jobNo, return_arguments = s.run_job (runTrue, {}) 93 | t.assertEquals (error, 0) 94 | -- t.assertEquals (jobNo, 0) 95 | -- t.assertIsTable (return_arguments) 96 | end 97 | 98 | function TestScheduler:test_run_target () 99 | local runTarget = { run = function (devNo) return devNo == 42 end} 100 | local error, error_msg, jobNo, return_arguments = s.run_job (runTarget, {}, nil, 42) 101 | t.assertEquals (error, 0) 102 | -- t.assertEquals (jobNo, 0) 103 | -- t.assertIsTable (return_arguments) 104 | end 105 | 106 | function TestScheduler:test_null_job () 107 | local nullJob = { } 108 | local error, error_msg, jobNo, return_arguments = s.run_job (nullJob, {}) 109 | t.assertEquals (error, 0) 110 | -- t.assertEquals (jobNo, 0) 111 | -- t.assertIsTable (return_arguments) 112 | end 113 | 114 | function TestScheduler:test_run_false () 115 | local runFalse = { run = function () return false end} 116 | local error, error_msg, jobNo, return_arguments = s.run_job (runFalse, {}) 117 | t.assertEquals (error, -1) 118 | -- t.assertEquals (jobNo, 0) 119 | -- t.assertIsTable (return_arguments) 120 | end 121 | 122 | --local pretty = require "pretty" 123 | 124 | function TestScheduler:test_job_done () 125 | local jobDone = { job = function (...) 126 | -- print ("TEST_JOB_DONE", pretty {...}) 127 | return s.state.Done 128 | end} 129 | local error, error_msg, jobNo, return_arguments = s.run_job (jobDone, {}) 130 | t.assertEquals (error, 0) 131 | t.assertIsNumber (jobNo) 132 | t.assertNotEquals (jobNo, 0) 133 | t.assertIsTable (return_arguments) 134 | s.TEST.step () -- one cycle of processing 135 | local status, notes = s.status (jobNo) 136 | t.assertEquals (status, s.state.Done) 137 | t.assertEquals (notes, '') 138 | end 139 | 140 | function TestScheduler:test_job_target () 141 | local jobTarget = { 142 | job = function (devNo) 143 | if devNo == 42 144 | then return s.state.Done 145 | else return s.state.Error 146 | end 147 | end} 148 | local error, error_msg, jobNo, return_arguments = s.run_job (jobTarget, {}, nil, 42) 149 | t.assertEquals (error, 0) 150 | t.assertIsNumber (jobNo) 151 | t.assertNotEquals (jobNo, 0) 152 | t.assertIsTable (return_arguments) 153 | s.TEST.step (1) -- one cycle of processing 154 | local status, notes = s.status (jobNo) 155 | -- t.assertEquals (status, s.state.Done) 156 | -- t.assertEquals (notes, '') 157 | end 158 | 159 | function TestScheduler:test_job_error () 160 | local jobError = { job = function () return s.state.Error end} 161 | local error, error_msg, jobNo, return_arguments = s.run_job (jobError, {}) 162 | t.assertEquals (error, 0) 163 | t.assertIsNumber (jobNo) 164 | t.assertNotEquals (jobNo, 0) 165 | t.assertIsTable (return_arguments) 166 | s.TEST.step () -- one cycle of processing 167 | local status, notes = s.status (jobNo) 168 | t.assertEquals (status, s.state.Error) 169 | t.assertEquals (notes, '') 170 | end 171 | 172 | function TestScheduler:test_call () 173 | local error, error_msg, jobNo, return_arguments = s.run_job (myJob, {}) 174 | t.assertEquals (error, 0) 175 | t.assertIsNumber (jobNo) 176 | t.assertIsTable (return_arguments) 177 | 178 | local status, notes = s.status (jobNo) 179 | t.assertEquals (status, s.state.WaitingToStart) 180 | t.assertEquals (notes, '') 181 | 182 | for _, seq in ipairs (sequence) do 183 | s.TEST.step () 184 | local status, notes = s.status (jobNo) 185 | t.assertEquals (notes, '') 186 | t.assertEquals (status, seq) 187 | end 188 | end 189 | 190 | function TestScheduler:test_delayed () 191 | local Ndelay = 0 192 | local delay_sequence = {s.state.WaitingToStart, s.state.Done} 193 | local delayed = { 194 | job = function () Ndelay = Ndelay+1; return delay_sequence [Ndelay], TIMEOUT end 195 | } 196 | local error, error_msg, jobNo, return_arguments = s.run_job (delayed, {}) 197 | t.assertEquals (error, 0) 198 | t.assertIsNumber (jobNo) 199 | t.assertIsTable (return_arguments) 200 | 201 | local status, notes = s.status (jobNo) 202 | t.assertEquals (status, s.state.WaitingToStart) 203 | t.assertEquals (notes, '') 204 | 205 | s.TEST.step() -- should stay in waiting to start status and then run after timeout period 206 | 207 | local status, notes = s.status (jobNo) 208 | t.assertEquals (status, s.state.WaitingToStart) 209 | t.assertEquals (notes, '') 210 | 211 | socket.select ({}, nil, TIMEOUT) -- wait a bit 212 | 213 | s.TEST.step() -- should run to completion 214 | 215 | local status, notes = s.status (jobNo) 216 | t.assertEquals (status, s.state.Done) 217 | t.assertEquals (notes, '') 218 | end 219 | 220 | function TestScheduler:test_no_timeout_tag () 221 | local timeout = { 222 | job = function () return s.state.WaitingForCallback, TIMEOUT end, -- wait forever ! 223 | } 224 | local error, error_msg, jobNo, return_arguments = s.run_job (timeout, {}) 225 | t.assertEquals (error, 0) 226 | t.assertIsNumber (jobNo) 227 | t.assertIsTable (return_arguments) 228 | 229 | local status, notes = s.status (jobNo) 230 | t.assertEquals (status, s.state.WaitingToStart) 231 | t.assertEquals (notes, '') 232 | 233 | s.TEST.step() -- should now be in WaitingForCallback forever 234 | 235 | local status, notes = s.status (jobNo) 236 | t.assertEquals (status, s.state.WaitingForCallback) 237 | t.assertEquals (notes, '') 238 | 239 | socket.select ({}, nil, TIMEOUT) -- wait a bit 240 | 241 | s.TEST.step() -- should timeout and exit with Aborted status 242 | 243 | local status, notes = s.status (jobNo) 244 | t.assertEquals (status, s.state.Aborted) 245 | t.assertEquals (notes, "no action tag specified for: timeout") 246 | end 247 | 248 | 249 | function TestScheduler:test_timeout_tag () 250 | local timeout = { 251 | job = function () return s.state.WaitingForCallback, TIMEOUT end, -- wait forever ! 252 | timeout = function () return s.state.Done, 0 end 253 | } 254 | local error, error_msg, jobNo, return_arguments = s.run_job (timeout, {}) 255 | t.assertEquals (error, 0) 256 | t.assertIsNumber (jobNo) 257 | t.assertIsTable (return_arguments) 258 | 259 | local status, notes = s.status (jobNo) 260 | t.assertEquals (status, s.state.WaitingToStart) 261 | t.assertEquals (notes, '') 262 | 263 | s.TEST.step() -- should now be in WaitingForCallback forever 264 | 265 | local status, notes = s.status (jobNo) 266 | t.assertEquals (status, s.state.WaitingForCallback) 267 | t.assertEquals (notes, '') 268 | 269 | socket.select ({}, nil, TIMEOUT) -- wait a bit 270 | 271 | s.TEST.step() -- should timeout and exit with Done status 272 | 273 | local status, notes = s.status (jobNo) 274 | t.assertEquals (status, s.state.Done) 275 | t.assertEquals (notes, '') 276 | end 277 | 278 | function TestScheduler:test_invalid_state () 279 | local invalid = { 280 | job = function () return 42,0 end 281 | } 282 | local error, error_msg, jobNo, return_arguments = s.run_job (invalid, {}) 283 | t.assertEquals (error, 0) 284 | t.assertIsNumber (jobNo) 285 | t.assertIsTable (return_arguments) 286 | 287 | s.TEST.step() -- should now have exited with an invalid state 288 | 289 | local status, notes = s.status (jobNo) 290 | t.assertEquals (status, s.state.Aborted) 291 | t.assertEquals (notes, "invalid job state returned: 42") 292 | end 293 | 294 | 295 | function TestScheduler:test_context () 296 | local function fct (x) 297 | t.assertEquals (x, math.pi) 298 | -- t.assertEquals (luup.device, 42) 299 | return 888 300 | end 301 | local ok,y = s.context_switch (42, fct, math.pi) 302 | t.assertEquals (ok, true) 303 | t.assertIsNil (luup.device) 304 | t.assertEquals (y, 888) 305 | end 306 | 307 | 308 | function TestScheduler:test_action_returns () 309 | luup = require "openLuup.luup" 310 | local devNo = luup.create_device ("my_device_type") -- create a device 311 | 312 | luup.variable_set ("my_service_id", "number", 42, devNo) -- and a serviceId with variable 313 | local action_returns = { 314 | serviceId = "my_service_id", -- define action as being in same service 315 | run = function () return true end, -- run does nothing itself... 316 | returns = {FancyOutputName = "number"}, -- ...but should return the state variable 317 | } 318 | local error, error_msg, jobNo, return_arguments = s.run_job (action_returns, {}) 319 | t.assertEquals (error, 0) 320 | -- t.assertEquals (jobNo, 0) 321 | t.assertIsTable (return_arguments) 322 | -- t.assertEquals (#return_arguments, 1) 323 | -- t.assertEquals (return_arguments.FancyOutputName, 42) 324 | end 325 | 326 | 327 | function TestScheduler:test_ () 328 | 329 | end 330 | 331 | 332 | ------------------- 333 | 334 | if not multifile then t.LuaUnit.run "-v" end 335 | 336 | ------------------- 337 | -------------------------------------------------------------------------------- /tests/test_server.lua: -------------------------------------------------------------------------------- 1 | 2 | local t = require "tests.luaunit" 3 | 4 | -- openLuup.server TESTS for: 5 | -- 6 | -- basic utilities, 7 | -- wget client (both internal and external HTTP(s) requests), 8 | -- the three main request types: 9 | -- 1) files 10 | -- 2) Luup data_request?id=... 11 | -- 3) CGIs implementented with Lua WSAPI 12 | -- 13 | 14 | local s = require "openLuup.http" 15 | local s2 = require "openLuup.servlet" 16 | 17 | local json = require "openLuup.json" 18 | 19 | 20 | TestServerUtilities = {} 21 | 22 | function TestServerUtilities:test_methodlist () 23 | t.assertIsFunction (s.add_callback_handlers) 24 | t.assertIsFunction (s.wget) 25 | t.assertIsFunction (s.start) 26 | end 27 | 28 | function TestServerUtilities:test_myip () 29 | local ip = s.myIP 30 | t.assertIsString (ip) 31 | local syntax = "%d+%.%d+%.%d+%.%d+" 32 | t.assertTrue (ip: match (syntax)) 33 | end 34 | 35 | function TestServerUtilities:test_CamelCaps () 36 | local cc = s.TEST.CamelCaps 37 | local h = "a-REALLY-Strange-hEADER-12345-isnt-it" 38 | local H = "A-Really-Strange-Header-12345-Isnt-It" 39 | t.assertEquals (cc(h), H) 40 | end 41 | 42 | function TestServerUtilities:test_content_iterator () 43 | local content = "abc123" 44 | local mc = s.TEST.make_content 45 | local mi = s2.TEST.make_iterator 46 | local i = mi (content) 47 | t.assertIsFunction (i) -- this is the iterator function 48 | local c = i() 49 | t.assertIsString (c) -- this is the recovered content 50 | t.assertEquals (c, content) 51 | local c2 = i() -- there should be no more... 52 | t.assertIsNil (c2) 53 | end 54 | 55 | function TestServerUtilities:test_request_object () 56 | local ro = s.TEST.request_object 57 | local u = "http://127.0.0.1:3480/data_request?id=testing&p1=abc&p2=123" 58 | local o = ro(u) 59 | 60 | local correct = { 61 | URL = { 62 | authority = "127.0.0.1:3480", 63 | host = "127.0.0.1", 64 | path = "/data_request", 65 | port = "3480", 66 | query = "id=testing&p1=abc&p2=123", 67 | scheme = "http" 68 | }, 69 | handler = s.TEST.data_request, 70 | headers = {}, 71 | http_version = "HTTP/1.1", 72 | internal = true, 73 | method = "GET", 74 | parameters = { 75 | id = "testing", 76 | p1 = "abc", 77 | p2 = "123" 78 | }, 79 | path_list={"data_request", is_absolute=1}, 80 | post_content = "" 81 | } 82 | 83 | o.request_start = nil -- can't know what this will be 84 | t.assertItemsEquals (o, correct) 85 | end 86 | 87 | TestServerRequests = {} 88 | 89 | 90 | function TestServerRequests:test_http_file () 91 | local hf = s2.TEST.http_file 92 | local ro = s.TEST.request_object 93 | local ob = ro "http:localhost:3480/index.html" 94 | local s,h,i = hf (ob) 95 | t.assertIsNumber (s) 96 | t.assertIsTable (h) 97 | t.assertIsFunction (i) 98 | local f = i() 99 | t.assertIsString (f) 100 | t.assertEquals (h["Content-Type"], "text/html") 101 | end 102 | 103 | function TestServerRequests:test_http_file_not_found () 104 | local hf = s2.TEST.http_file 105 | local ro = s.TEST.request_object 106 | local s,h,i = hf (ro "http:localhost:3480/qwertyuiop") 107 | t.assertIsNumber (s) 108 | t.assertIsTable (h) 109 | t.assertIsFunction (i) 110 | local f = i() 111 | t.assertIsString (f) 112 | t.assertEquals (f, "file not found:qwertyuiop") 113 | t.assertEquals (s, 404) 114 | end 115 | 116 | function TestServerRequests:test_data_request () 117 | local dr = s2.TEST.data_request 118 | local ro = s.TEST.request_object 119 | -- special TEST request returns JSON-encoded handler parameter list 120 | local ob = ro "http://127.0.0.1:3480/data_request?id=TEST&p1=abc&p2=123" 121 | local s,h,i = dr (ob) 122 | t.assertIsNumber (s) 123 | t.assertIsTable (h) 124 | t.assertIsFunction (i) 125 | t.assertEquals (s, 200) -- check the status 126 | t.assertEquals (h["Content-Type"], "application/json") -- check the content type header 127 | local f = i() -- this is the content iterator 128 | t.assertIsString (f) 129 | local p = json.decode (f) -- decode the list and check the parameters! 130 | t.assertEquals (p[1], "TEST") 131 | t.assertIsTable (p[2]) 132 | t.assertEquals (p[2].p1, "abc") 133 | t.assertEquals (p[2].p2, "123") 134 | end 135 | 136 | function TestServerRequests:test_wsapi_cgi () 137 | local cg = s2.TEST.wsapi_cgi 138 | local ro = s.TEST.request_object 139 | local u = "http://0.0.0.0:3480/cgi-bin/cmh/sysinfo.sh" -- CGI requests are handled by WSAPI 140 | local o = ro(u) 141 | local s,h,i = cg (o) 142 | t.assertIsNumber (s) 143 | t.assertIsTable (h) 144 | t.assertIsFunction (i) 145 | local f = i() 146 | t.assertIsString (f) 147 | t.assertEquals (h["Content-Type"], "text/plain") 148 | end 149 | 150 | TestServerResponses = {} 151 | 152 | 153 | function TestServerResponses:test_response_simple () 154 | local re = s.TEST.http_response 155 | local mi = s2.TEST.make_iterator 156 | local status = 200 157 | local content = "Test content" 158 | local headers = { 159 | ["Content-Length"] = #content, 160 | ["Content-Type"] = "text/plain", 161 | } 162 | -- headers, response, chunked = http_response (status, headers, iterator) 163 | local h,r,c = re (status, headers, mi (content)) 164 | t.assertIsString (h) 165 | t.assertIsString (r) 166 | t.assertFalse (c) 167 | t.assertNotNil (h:match "%C\r\n\r\n$") -- headers should end with blank line 168 | t.assertEquals (r, content) 169 | end 170 | 171 | 172 | function TestServerResponses:test_response_chunked () 173 | local re = s.TEST.http_response 174 | local mi = s2.TEST.make_iterator 175 | local status = 200 176 | local content = "Test content" 177 | local headers = { 178 | -- ["Content-Length"] = #content, -- no content lengths signal chunked 179 | ["Content-Type"] = "text/plain", 180 | } 181 | -- headers, response, chunked = http_response (status, headers, iterator) 182 | local h,r,c = re (status, headers, mi (content)) 183 | t.assertIsString (h) 184 | t.assertIsString (r) 185 | t.assertTrue (c) 186 | t.assertNotNil (h:match "%C\r\n\r\n$") -- headers should end with blank line 187 | t.assertEquals (r, content) 188 | end 189 | 190 | 191 | TestServerWGET = {} 192 | 193 | 194 | function TestServerWGET:test_internal () -- same as data_request?id=TEST above, but using WGET API 195 | local wget = s.wget 196 | -- special TEST request returns JSON-encoded handler parameter list 197 | local status,r = wget "http://localhost:3480/data_request?id=TEST&p1=abc&p2=123" 198 | t.assertIsNumber (status) 199 | t.assertIsString (r) 200 | t.assertEquals (status, 0) -- check the status 201 | local p = json.decode (r) -- decode the list and check the parameters! 202 | t.assertEquals (p[1], "TEST") 203 | t.assertIsTable (p[2]) 204 | t.assertEquals (p[2].p1, "abc") 205 | t.assertEquals (p[2].p2, "123") 206 | end 207 | 208 | function TestServerWGET:test_external_http () 209 | local wget = s.wget 210 | local status,r = wget "http://www.google.com" 211 | t.assertEquals (status,0) 212 | t.assertIsString (r) 213 | t.assertTrue (r: match "^") -- check start and end of HTML 214 | t.assertTrue (r: match "%c*$") 215 | end 216 | 217 | function TestServerWGET:test_external_https () 218 | local wget = s.wget 219 | local status,r = wget "https://www.google.com" 220 | -- t.assertIsNumber (status) 221 | t.assertIsString (r) 222 | end 223 | 224 | -------------------- 225 | 226 | if not multifile then t.LuaUnit.run "-v" end 227 | 228 | -------------------- 229 | -------------------------------------------------------------------------------- /tests/test_template.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup TEST template 4 | 5 | luup = require "openLuup.luup" 6 | 7 | 8 | Test = {} 9 | 10 | function Test:setUp () 11 | end 12 | 13 | 14 | 15 | function Test:test_ () 16 | 17 | end 18 | 19 | --------------------- 20 | 21 | if multifile then return end 22 | t.LuaUnit.run "-v" 23 | 24 | --------------------- 25 | 26 | -------------------------------------------------------------------------------- /tests/test_timers.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- openLuup TIMER tests 4 | 5 | -- 6 | -- 7 | local timers = require "openLuup.timers" 8 | 9 | luup = luup or {} 10 | luup.latitude = 51.75 11 | luup.longitude = -1.4 12 | 13 | TestTimers = {} -- timer tests 14 | 15 | function TestTimers:setUp () 16 | end 17 | 18 | function TestTimers:tearDown () 19 | end 20 | 21 | -- basics 22 | 23 | function TestTimers:test_methods_present () 24 | t.assertIsFunction (timers.sunrise) 25 | t.assertIsFunction (timers.sunset) 26 | t.assertIsFunction (timers.is_night) 27 | t.assertIsFunction (timers.call_delay) 28 | t.assertIsFunction (timers.call_timer) 29 | end 30 | 31 | -- individual functions 32 | 33 | function TestTimers:test_night() 34 | t.assertIsBoolean (timers.is_night()) 35 | end 36 | 37 | function TestTimers:test_delay() 38 | local function fct () end 39 | timers.call_delay (fct, 42, {"some data", test = 123}) 40 | end 41 | 42 | function TestTimers:test_invalid_timer() 43 | -- Type is 1=Interval timer, 2=Day of week timer, 3=Day of month timer, 4=Absolute timer. 44 | -- call_timer (fct, type, time, days, data) 45 | local ok 46 | local function fct () end 47 | ok = timers.call_timer (fct, 42, "5m", nil, "some string data") 48 | t.assertNotEquals (ok, 0) 49 | end 50 | 51 | 52 | function TestTimers:test_interval_timer() 53 | -- Type is 1=Interval timer. 54 | -- For an interval timer, days is not used, and 55 | -- Time should be a number of seconds, minutes, or hours using an optional 'h' or 'm' suffix. 56 | local ok 57 | local function fct () end 58 | ok = timers.call_timer (fct, 1, "5m", nil, "some string data") -- shouldn't fail 59 | t.assertEquals (ok, 0) 60 | end 61 | 62 | 63 | function TestTimers:test_day_of_week_timer() 64 | -- Type 2=Day of week timer. 65 | -- Days is a comma separated list with the days of the week where 1=Monday and 7=Sunday. 66 | -- Time is the time of day in hh:mm:ss format. 67 | -- Time can also include an 'r' at the end for Sunrise or a 't' for Sunset 68 | -- and the time is relative to sunrise/sunset. 69 | local ok 70 | local function fct () end 71 | ok = timers.call_timer (fct, 2, "12:00:00", "1,3,5", {"some data"}) -- shouldn't fail 72 | t.assertEquals (ok, 0) 73 | end 74 | 75 | function TestTimers:test_day_of_month_timer() 76 | -- Type 3=Day of month timer. 77 | -- Day of month works the same way except 78 | -- Days is a comma separated list of days of the month, such as "15,20,30". 79 | local ok 80 | local function fct () end 81 | ok = timers.call_timer (fct, 3, "13:14:15", "14,28", "some string data") -- shouldn't fail 82 | t.assertEquals (ok, 0) 83 | end 84 | 85 | function TestTimers:test_absolute_timer() 86 | -- Type 4=Absolute timer. 87 | -- absolute timer implemented using delay (one-shot only) 88 | -- Days is not used, and Time should be in the format: "yyyy-mm-dd hh:mm:ss" 89 | local ok 90 | local function fct () end 91 | ok = timers.call_timer (fct, 4, "2015-08-18 06:00:00", nil, "some string data") -- shouldn't fail 92 | t.assertEquals (ok, 0) 93 | end 94 | 95 | 96 | function TestTimers:test_sun_relative() 97 | -- Type 4=Absolute timer. 98 | -- absolute timer implemented using delay (one-shot only) 99 | -- Days is not used, and Time should be in the format: "yyyy-mm-dd hh:mm:ss" 100 | local ok 101 | local function fct () end 102 | ok = timers.call_timer (fct, 3, "-01:30:00r", "14,28", "some string data") -- shouldn't fail 103 | t.assertEquals (ok, 0) 104 | end 105 | 106 | -------------------- 107 | 108 | TestTimersOther = {} 109 | 110 | 111 | -- see: http://forum.micasaverde.com/index.php/topic,38818.0.html 112 | -- the error is: 113 | -- "We are monday 09:00, and the scheduled is for tuesday 10:00. 114 | -- As 10:00 > 09:00, it won't add the offset, and schedule will be on monday 10:00." 115 | function TestTimersOther:test_vosmont_DoW () 116 | -- Type 2=Day of week timer. 117 | -- Days is a comma separated list with the days of the week where 1=Monday and 7=Sunday. 118 | -- Time is the time of day in hh:mm:ss format. 119 | do return end -- TODO: fix this - it may fail incorrectly! 120 | -- ********************** 121 | local ok, due 122 | local function fct () end 123 | local function dt(t) return os.date ("%c", t) end 124 | 125 | local now = os.time () 126 | local hence = now + 25 * 60 * 60 -- one hour and one day later 127 | local thence = os.date ("%H:%M:%S", hence) 128 | ok,_,_,_,due = timers.call_timer (fct, 2, thence, "2,3,4,5,6,7", {"DoW @vosmont: " .. thence}) -- shouldn't fail 129 | t.assertEquals (ok, 0) 130 | local expected = now + 25 * 60 * 60 131 | t.assertIsNumber (due) 132 | t.assertEquals (dt(due), dt(expected)) -- check the right time 133 | end 134 | 135 | 136 | function TestTimersOther:test_day_timer() 137 | -- Type is 1=Interval timer. 138 | -- For an interval timer, days is not used, and 139 | -- Time should be a number of seconds, minutes, or hours using an optional 'h' or 'm' suffix. 140 | -- 2019.05.03 also 'd' !!! 141 | local ok, due 142 | local function fct () end 143 | local function dt(t) return os.date ("%c", t) end 144 | 145 | local now = os.time () 146 | ok,_,_,_,due = timers.call_timer (fct, 1, "1d", nil, "some string data") -- shouldn't fail 147 | t.assertEquals (ok, 0) 148 | local expected = now + 24 * 60 * 60 149 | t.assertIsNumber (due) 150 | t.assertEquals (dt(due), dt(expected)) -- check the right time 151 | end 152 | 153 | TestRiseSet = {} 154 | 155 | 156 | function TestRiseSet:test_sunrise () 157 | local s = timers.sunrise () 158 | local now = os.time() 159 | t.assertTrue (s > now) -- later than now... 160 | t.assertTrue (s < now + 24*60*60) -- ...but earlier than this time tomorrow 161 | end 162 | 163 | function TestRiseSet:test_sunset () 164 | local s = timers.sunset () 165 | local now = os.time() 166 | t.assertTrue (s > now) -- later than now... 167 | t.assertTrue (s < now + 24*60*60) -- ...but earlier than this time tomorrow 168 | end 169 | 170 | local function datetime (...) 171 | local x = {"-----"} 172 | for _,t in ipairs {...} do 173 | x[#x+1] = os.date ("%c", t) 174 | end 175 | return table.concat (x, "\n ") 176 | end 177 | 178 | function TestRiseSet:test_rise_set () 179 | -- London, Greenwich 180 | local latitude = 51.5 181 | local longitude = 0 182 | local date, sunrise 183 | print "\n----------" 184 | local rs = timers.TEST.rise_set 185 | 186 | date = { 187 | {year = 1980, month = 1, day = 1}, 188 | {year = 2000, month = 6, day = 11}, 189 | {year = 2016, month = 10, day = 21}, 190 | } 191 | 192 | sunrise = { -- all in UTC 193 | {year = 1980, month = 1, day = 1, hour = 8, min = 6, isdst = false}, 194 | {year = 2000, month = 6, day = 11, hour = 3, min = 43, isdst = false}, 195 | {year = 2016, month = 10, day = 21, hour = 6, min = 35, isdst = false}, 196 | } 197 | 198 | for i,d in ipairs (date) do 199 | local r,s,n = rs(d, latitude, longitude) 200 | print (os.difftime (r, os.time(sunrise[i]))) 201 | print (datetime(r,s,n)) 202 | end 203 | 204 | print "----------" 205 | 206 | -- San Francisco 207 | latitude = 37 + 46/60 208 | longitude = - (122 + 26/60) 209 | 210 | sunrise = { -- all in UTC 211 | {year = 1980, month = 1, day = 1, hour = 8 + 7, min = 25, isdst = false}, 212 | {year = 2000, month = 6, day = 11, hour = 3 + 9, min = 47, isdst = false}, 213 | {year = 2016, month = 10, day = 21, hour = 6 + 8, min = 25, isdst = false}, --????? 214 | } 215 | 216 | for i,d in ipairs (date) do 217 | local r,s,n = rs(d, latitude, longitude) 218 | print (os.difftime (r, os.time(sunrise[i]))) 219 | print (datetime(r,s,n)) 220 | end 221 | 222 | 223 | end 224 | 225 | -------------------- 226 | 227 | if not multifile then t.LuaUnit.run "-v" end 228 | 229 | -------------------- 230 | -------------------------------------------------------------------------------- /tests/test_userdata.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- Device Files module tests 4 | 5 | local userdata = require "openLuup.userdata" 6 | 7 | TestUserData = {} 8 | 9 | function TestUserData:test_save_load () 10 | -- test save 11 | -- local scene = {user_table = function () return {a=1, b= 2, c = "42"} end} 12 | -- local x = {rooms = {"room1", nil, "room3"}, scenes = {scene}} 13 | -- local ok, msg = userdata.save (x, "tests/data/testuserdata.json") 14 | -- t.assertTrue (ok) 15 | -- t.assertIsNil (msg) 16 | 17 | --test_load 18 | 19 | -- local x, msg = userdata.load "tests/testuserdata.json" 20 | -- t.assertIsNil (msg) 21 | -- t.assertIsTable (x) 22 | -- t.assertIsTable (x.scenes) 23 | -- t.assertIsTable (x.rooms) 24 | -- t.assertEquals (#x.rooms, 2) 25 | -- t.assertEquals (x.rooms[2].name, "room3") 26 | -- t.assertEquals (x.scenes[1].c, "42") 27 | end 28 | 29 | 30 | ------------------- 31 | 32 | if multifile then return end 33 | 34 | t.LuaUnit.run "-v" 35 | 36 | ------------------- 37 | -------------------------------------------------------------------------------- /tests/test_vfs.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- virtualfilesystem module tests 4 | 5 | local vfs = require "openLuup.virtualfilesystem" 6 | 7 | 8 | TestVFS = {} 9 | 10 | 11 | function TestVFS:test_io () 12 | local test_string = "this is only a test" 13 | 14 | f = vfs.open ("vfs_test", 'w') 15 | f:write (test_string) 16 | f:close () 17 | 18 | f = vfs.open "vfs_test" 19 | local x = f:read () 20 | f:close () 21 | t.assertEquals (x, test_string) 22 | end 23 | 24 | function TestVFS:test_open_for_read_fail () 25 | local f, m = vfs.open "xyz" 26 | t.assertIsNil (f) 27 | t.assertIsString (m) 28 | end 29 | 30 | function TestVFS:test_open_for_read_ok () 31 | local f, m = vfs.open "index.html" 32 | t.assertIsTable (f) 33 | t.assertIsFunction (f.read) 34 | t.assertIsFunction (f.close) 35 | t.assertIsNil (m) 36 | end 37 | 38 | ------------------- 39 | 40 | if multifile then return end 41 | 42 | t.LuaUnit.run "-v" 43 | 44 | print ("TOTAL number of tests run = ", N) 45 | 46 | ------------------- 47 | 48 | -------------------------------------------------------------------------------- /tests/test_whisper.lua: -------------------------------------------------------------------------------- 1 | -- test Whisper database 2 | 3 | local whisper = require "L_DataWhisper" 4 | 5 | whisper.debugOn = true 6 | whisper.CACHE_HEADERS = true 7 | 8 | VERIFY = true 9 | -- utilities 10 | 11 | 12 | local function array_equality (a,b) 13 | if #a ~= #b then return end 14 | for i = 1,#a do 15 | if a[i] ~= b[i] then return end 16 | end 17 | return true 18 | end 19 | 20 | if not print then print = function (...) AKB.log (table.concat ({...}, ' ')) end; end 21 | 22 | -- tests 23 | 24 | local function test_2 () -- one element store! 25 | local name = "test-2" 26 | print (name) 27 | local db = (name..".wsp") 28 | os.remove (db) 29 | print "create:" 30 | whisper.create (db, {{1,1}}, 0, "last") -- check binary retentions syntax 31 | print ('',whisper.info (db)) 32 | local input = {42} 33 | print "write" 34 | local start = 1e9 35 | local T = start 36 | whisper.update (db, input[1], T, T) 37 | 38 | print "read" 39 | local tv = whisper.fetch (db, 0, nil, T) 40 | for i, v,t in tv:ipairs () do 41 | print (i, t,v) 42 | end 43 | if VERIFY then assert (array_equality (tv.values,input), name .. ": input ~= output") end 44 | 45 | print '' 46 | end 47 | 48 | local function test_1 () 49 | local name = "test-1" 50 | print (name) 51 | local db = (name..".wsp") 52 | local retentions = "1:2" 53 | os.remove (db) 54 | print "create" 55 | whisper.create (db, {{1,2}}, 0, "last") -- check binary retentions too 56 | print ('',whisper.info (db)) 57 | 58 | local input = {1,2} 59 | print "write" 60 | local start = 1e9 61 | local T 62 | for i = 1,#input do 63 | T = start + i - 1 64 | whisper.update (db, input[i], T, T) 65 | end 66 | 67 | print "read" 68 | local tv = whisper.fetch (db, 0, nil, T) 69 | for i, v,t in tv:ipairs () do 70 | print (i, t,v) 71 | end 72 | if VERIFY then assert (array_equality (tv.values,input), name .. ": input ~= output") end 73 | 74 | print '' 75 | end 76 | 77 | 78 | local function test0 () -- single archive 79 | local name = "test0" 80 | print (name) 81 | local db = (name..".wsp") 82 | local retentions = "1s:5" 83 | os.remove (db) 84 | print "create" 85 | whisper.create (db, retentions, 0, "last") 86 | print ('',whisper.info (db)) 87 | 88 | local input = {1,2,3,4,5} 89 | print "write" 90 | local start = 1e9 +3 91 | local T 92 | for i = 1,#input do 93 | T = start + i - 1 94 | whisper.update (db, input[i], T, T) 95 | end 96 | 97 | print "read" 98 | local tv = whisper.fetch (db, 0, nil, T) 99 | for i, v,t in tv:ipairs () do 100 | print (i, t,v) 101 | end 102 | if VERIFY then assert (array_equality (tv.values,input), name .. ": input ~= output") end 103 | 104 | print "exra point" 105 | T = T + 1 106 | whisper.update (db, 6, T,T) 107 | tv = whisper.fetch (db, 0, nil, T) 108 | for i, v,t in tv:ipairs () do 109 | print (i, t,v) 110 | end 111 | if VERIFY then assert (array_equality (tv.values,{2,3,4,5,6}), name ..": input ~= output") end 112 | print '' 113 | end 114 | 115 | 116 | local function test1 () -- multiple archives 117 | local name = "test1" 118 | print (name) 119 | local db = (name..".wsp") 120 | local retentions = "1:2,2:3,4:3" 121 | os.remove (db) 122 | print "create" 123 | whisper.create (db, retentions, 0, "sum") 124 | print ('',whisper.info (db)) 125 | 126 | local input = {1,2,3, 4, 5,6} 127 | -- local input = {1,nil,3,nil,5} 128 | local verify = {2, 4, 6} 129 | 130 | print "write" 131 | local start = 1e9 132 | local t = start 133 | -- single point - written OK to multiple archives ?? 134 | 135 | whisper.update (db, 42, t, t) 136 | local a = whisper.fetch (db, t,t,t) -- get current point 137 | assert (a.values[1] == 42, "current archive incorrect") 138 | local b = whisper.fetch (db, t-3,t,t) -- get current point 139 | for n, v,t in b:ipairs() do 140 | print (n, t,v) 141 | end 142 | -- assert (b.values[1] == 42, "previous archive incorrect") 143 | 144 | -- 145 | 146 | for i = 1,#input do 147 | t = start + i - 1 148 | whisper.update (db, input[i], t, t) 149 | end 150 | 151 | print "read" 152 | local tv = whisper.fetch (db, 0, nil, t) 153 | for i, v,t in tv:ipairs () do 154 | print (i, t,v) 155 | end 156 | -- if VERIFY then assert (array_equality (tv.values,verify), name .. ": input ~= output") end 157 | print '' 158 | end 159 | 160 | 161 | local function test2 () 162 | print "test2" 163 | local db = "test2.wsp" 164 | local retentions = "1:2, 2:3, 6:10" 165 | 166 | os.remove (db) 167 | print "create" 168 | whisper.create (db, retentions, 0, "sum") 169 | 170 | local info = whisper.info (db) 171 | print ('info:', info) 172 | print ("archives: ", tostring(info.retentions)) 173 | print "write" 174 | local start = 1e9 175 | local t 176 | for i = 1,30 do 177 | t = start + i - 1 178 | whisper.update (db, 1, t, t) 179 | end 180 | 181 | print "read first archive" 182 | local verify = {1,1} 183 | local tv = whisper.fetch (db, t-1,t, t) 184 | for i, v,t in tv:ipairs () do 185 | print (i, t,v) 186 | end 187 | if VERIFY then assert (array_equality (tv.values,verify), "test 2, archive 1: input ~= output") end 188 | 189 | print "read last archive" 190 | local verify = {6,6,6,6,6,6,6,6,6,6} 191 | local tv = whisper.fetch (db, 0, nil, t) 192 | for i, v,t in tv:ipairs () do 193 | print (i, t,v) 194 | end 195 | -- if VERIFY then assert (tv.values[2] == verify[2], "test 2, archive 2: input ~= output") end 196 | print '' 197 | end 198 | 199 | 200 | local function test3 () -- write performance 201 | print "test3" 202 | local db = "test3.wsp" 203 | local retentions = "10m:7d,1h:30d,3h:1y,1d:10y" 204 | local retentions = "1:1h" 205 | -- local retentions = "1s:1m,1m:1d,5m:7d,1h:90d,6h:1y,1d:5y" 206 | os.remove (db) 207 | print "create" 208 | print ("retentions: "..retentions) 209 | whisper.create (db, retentions, 0, "sum") 210 | print (whisper.info (db)) 211 | print "write" 212 | local t1 = os.time() 213 | local c1 = os.clock() 214 | local start = 1e9 215 | local t 216 | local N = 60*60 217 | for i = 1,N do 218 | t = start + i - 1 219 | whisper.update (db, i, t, t) 220 | end 221 | local t2 = os.time() 222 | local c2 = os.clock() 223 | local cpu_time = math.floor((c2-c1)*1e3) 224 | print ("elapsed time = ".. (t2-t1)..' S') 225 | print ("cpu time = ".. cpu_time..' mS') 226 | print ("time / point = ".. math.floor (1e3*cpu_time/N) .. ' µS') 227 | print '' 228 | end 229 | 230 | 231 | 232 | -- TESTS 233 | 234 | print "starting tests..." 235 | test_2() 236 | test_1() 237 | test0() 238 | test1() 239 | test2() 240 | 241 | whisper.debugOn = false 242 | 243 | test3() 244 | 245 | --local battery = "1d:1y" 246 | --local power = "20m:30d,3h:1y,1d:10y" 247 | --local THLG = "10m:7d,1h:30d,3h:1y,1d:10y" 248 | --local SECU = "1s:1m,1m:1d,5m:7d,1h:90d,6h:1y" 249 | 250 | print 'done' 251 | 252 | x = "1200:2160, 10800:2920, 86400:3650, 1:1000000" 253 | --print ( whisper.archiveSpec (x)) 254 | 255 | -------------------------------------------------------------------------------- /tests/test_xml.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | -- XML module tests 4 | 5 | local X = require "openLuup.xml" 6 | 7 | local N = 0 -- total test count 8 | 9 | 10 | --- XML decode validity tests 11 | 12 | local function invalid_decode (x, y) 13 | N = N + 1 14 | local lua = X.decode (x) .documentElement 15 | t.assertEquals (lua, nil) 16 | end 17 | 18 | 19 | -- INVALID 20 | 21 | TestDecodeInvalid = {} 22 | 23 | function TestDecodeInvalid:test_decode () 24 | -- invalid code just returns empty table 25 | invalid_decode ("rubbish", {}) 26 | invalid_decode ("", {}) 28 | invalid_decode (">", {}) 29 | end 30 | 31 | 32 | -- VALID 33 | 34 | TestDOMNavigation = {} 35 | 36 | function TestDOMNavigation:test_navigation_links() 37 | local x = X.decode [[ 38 | 39 | 40 | 41 | major 42 | minor 43 | minimus 44 | 45 | 46 | 47 | ]] .documentElement 48 | local p = x:getElementsByTagName "parent" 49 | t.assertIsTable (p) 50 | t.assertEquals (#p, 1) 51 | t.assertEquals (p[1][0], "parent") 52 | local c = p[1] 53 | t.assertEquals (#c, 3) 54 | t.assertEquals (p[1].firstChild[1], "major") 55 | t.assertEquals (p[1].lastChild[1], "minimus") 56 | local min = p[1][2] 57 | -- local maj = min.previousSibling 58 | -- local mus = min.nextSibling 59 | -- t.assertEquals (min[1], "minor") 60 | -- t.assertEquals (maj[1], "major") 61 | -- t.assertEquals (mus[1], "minimus") 62 | -- t.assertIsNil (maj.previousSibling) 63 | -- t.assertIsNil (mus.nextSibling) 64 | -- local g = p[1][-1] 65 | -- t.assertEquals (g[-1][0], "root") 66 | -- t.assertIsNil (g[-1][-1]) 67 | -- t.assertEquals (g[0], "grandparent") 68 | end 69 | 70 | function TestDOMNavigation:test_xpath_navigation() 71 | local d = X.decode [[ 72 | 73 | 74 | 75 | major 76 | minor 77 | minimus 78 | 79 | 80 | 81 | ]] 82 | local c = d.xpath (d.documentElement, "//grandparent/parent/child") 83 | 84 | t.assertEquals (#c, 3) 85 | local maj = c[1] 86 | local min = c[2] 87 | local mus = c[3] 88 | t.assertEquals (min[1], "minor") 89 | t.assertEquals (maj[1], "major") 90 | t.assertEquals (mus[1], "minimus") 91 | end 92 | 93 | 94 | TestDecodeValid = {} 95 | 96 | function TestDecodeValid:test_decode_simple () 97 | local s = X.decode [[ 98 | 99 | bung1 100 | bung2 101 | 102 | ]] 103 | 104 | t.assertIsTable (s) 105 | t.assertEquals (#s, 1) 106 | t.assertEquals (s[1][0], "foo") 107 | local c = s[1] 108 | t.assertEquals (#c, 2) 109 | t.assertEquals (c[1][0], "garp") 110 | t.assertEquals (c[2][0], "garp") 111 | t.assertEquals (c[1][1], "bung1") 112 | t.assertEquals (c[2][1], "bung2") 113 | end 114 | 115 | function TestDecodeValid:test_decode_text_node () 116 | local tn = X.decode [[ plain text ]] 117 | t.assertEquals (tn[1].a1, "one") 118 | t.assertEquals (tn[1][1], "plain text") -- note the surrounding spaces are gone 119 | end 120 | 121 | function TestDecodeValid:test_decode_simple_self_closing () 122 | local a = X.decode "" 123 | t.assertEquals (a[1][0], "foo") 124 | end 125 | 126 | function TestDecodeValid:test_decode_self_closing () 127 | local a = X.decode [[ ]] 128 | t.assertEquals (a[1].at, [[<>"'&]]) 129 | t.assertEquals (a[1].a2, "two") 130 | end 131 | 132 | function TestDecodeValid:test_decode_attributes () 133 | local a = X.decode '' 134 | t.assertEquals (a[1].at, [[<>"'&]]) 135 | end 136 | 137 | function TestDecodeValid:test_decode_escapes () 138 | local e = X.decode '<>"'&' 139 | t.assertEquals (e[1][1], [[<>"'&]]) 140 | end 141 | 142 | function TestDecodeValid:test_mixed_content () 143 | local e = X.decode " one three five" 144 | local c = e.documentElement 145 | -- note that the intervening text is ignored 146 | t.assertEquals (#c, 2) 147 | t.assertEquals (c[1][0], "two") 148 | t.assertEquals (c[2][0], "four") 149 | end 150 | 151 | 152 | function TestDecodeValid:test_emptytag () 153 | local empty 154 | empty = X.decode " " 155 | t.assertIsTable (empty) 156 | t.assertEquals (#empty, 1) 157 | t.assertEquals (empty[1][0], "foo") 158 | t.assertEquals (empty[1][1], '') 159 | -- 160 | empty = X.decode " " 161 | t.assertIsTable (empty) 162 | t.assertEquals (#empty, 1) 163 | t.assertEquals (empty[1][0], "foo") 164 | t.assertEquals (empty[1][1], '') 165 | t.assertEquals (empty[1][1], '') 166 | end 167 | 168 | --- XML encode validity tests 169 | 170 | local function valid_encode (lua, v) 171 | N = N + 1 172 | local n,m = next (lua) 173 | local xml = tostring (X.TEST.createElement (n,{m})) 174 | t.assertIsString (xml) 175 | t.assertEquals (xml:gsub ("%s+", ' '), v) 176 | end 177 | 178 | 179 | 180 | -- VALID 181 | 182 | 183 | TestEncodeValid = {} 184 | 185 | 186 | function TestEncodeValid:test_literals () 187 | valid_encode ({["true"] = true}, "true ") 188 | valid_encode ({["false"] = false}, "false ") 189 | valid_encode ({["nil"] = nil}, " ") 190 | end 191 | 192 | function TestEncodeValid:test_numerics () 193 | local Inf = "8.88e888" 194 | valid_encode ({number = 42}, "42 ") 195 | end 196 | 197 | function TestEncodeValid:test_strings () 198 | valid_encode ({string="easy"}, "easy ") 199 | valid_encode ({ctrl="\n"}, " ") 200 | valid_encode ({UTF8= "1234 UTF-8 ß$¢€"}, "1234 UTF-8 ß$¢€ ") 201 | end 202 | 203 | function TestEncodeValid:test_escapes () 204 | local a = [[<>"'&]] -- characters to be escaped 205 | valid_encode ({escapes=a}, "<>"'& ") 206 | end 207 | 208 | -- Longer round-trip file tests 209 | 210 | local function round_trip_ok (x) 211 | local lua1 = X.decode (x) 212 | t.assertIsTable (lua1) 213 | local x2 = tostring (lua1.documentElement) -- not equal to x, necessarily, because of formatting and sorting 214 | t.assertIsString (x2) 215 | local lua2 = X.decode (x2) -- ...so go round once again 216 | t.assertIsTable (lua2) 217 | local x3,msg4 = tostring (lua2.documentElement) 218 | t.assertIsString (x3) 219 | t.assertEquals (x3, x2) -- should be the same this time around 220 | end 221 | 222 | 223 | local comprehensive = [[ 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | just the one 233 | the rain... 234 | ...in Spain 235 | <>"'& 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | escaped attributes 244 | 245 | 246 | 247 | 248 | level 1 249 | 250 | 1 251 | two 252 | a'b'c 253 | s"e"f 254 | 255 | 256 | 257 | one 258 | two 259 | three 260 | 261 | 262 | 263 | one 264 | 1 2 265 | un deux trois 266 | 267 | 268 | one 269 | two 270 | three 271 | 272 | 273 | 274 | 275 | 276 | 277 | a'b'c 278 | s"e"f 279 | 280 | 281 | 282 | ]] 283 | 284 | TestSameNestedTags = {} -- 2019.04.30 arising from loader error in service files 285 | 286 | function TestSameNestedTags:test_same_tags () 287 | local x = [[ 288 | 289 | 290 | 291 | Nested 292 | 293 | Middle 294 | 295 | Name 296 | 297 | ]] 298 | 299 | local d = X.decode (x) 300 | local top = d.documentElement 301 | t.assertEquals (top.nodeName, "top") 302 | for act in d.xpathIterator (top, "//middle") do 303 | for _,x in ipairs (act) do 304 | end 305 | end 306 | end 307 | 308 | 309 | 310 | TestEncodeDecode = {} 311 | 312 | function TestEncodeDecode:test_round_trip () 313 | -- round_trip_ok (comprehensive) 314 | end 315 | 316 | ------------------- 317 | 318 | if multifile then return end 319 | 320 | t.LuaUnit.run "-v" 321 | 322 | print ("TOTAL number of tests run = ", N) 323 | 324 | --do return end 325 | ------------------- 326 | 327 | local decode = X.decode 328 | -- TEST 329 | 330 | local x = "" 331 | local d = decode(x) 332 | local y = d.documentElement 333 | 334 | 335 | for z in d.nextNode (y, function (x, p) return p == "/a/b" end) do 336 | print (z[0], #z) 337 | end 338 | 339 | print "---- xpath" 340 | 341 | local w = d.xpath (y, "//b" ) 342 | for _,z in ipairs(w) do 343 | print (z[0], #z) 344 | end 345 | 346 | print "---- xpathIterator" 347 | 348 | for z in d.xpathIterator (y, "//b/*") do 349 | print (z[0], #z) 350 | end 351 | 352 | 353 | print "---- xpathIterator" 354 | 355 | for z in d.xpathIterator (y, "//*/f") do 356 | print (z[0], #z) 357 | end 358 | 359 | ----]] 360 | --------------------- 361 | 362 | --local pretty = require "pretty" 363 | 364 | ---- visual round-trip test 365 | 366 | --print "-----------" 367 | --local xmlDoc = X.decode(comprehensive) 368 | ----print(pretty(xmlDoc)) -- unwise to do this, since multiple sibling links make it appear extensive 369 | 370 | --print "-----------" 371 | 372 | 373 | --local a = X.simplify (xmlDoc.documentElement) 374 | --print(pretty(a)) 375 | 376 | --print (X.encode (a, "decoded-simplified")) 377 | 378 | --print 'done' 379 | 380 | ---- 381 | --[==[ 382 | local comprehensive = [[ 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | just the one 391 | the rain... 392 | ...in Spain 393 | <>"'& 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | escaped attributes 402 | 403 | 404 | 405 | 406 | level 1 407 | 408 | 1 409 | two 410 | a'b'c 411 | s"e"f 412 | 413 | 414 | 415 | one 416 | two 417 | three 418 | 419 | 420 | 421 | one 422 | 1 2 423 | un deux trois 424 | 425 | 426 | one 427 | two 428 | three 429 | 430 | 431 | 432 | 433 | 434 | 435 | a'b'c 436 | s"e"f 437 | 438 | 439 | 440 | ]] 441 | 442 | --]==] 443 | --- 444 | --local pretty = require "pretty" 445 | 446 | --local d = decode (comprehensive) 447 | ----print (pretty(d)) 448 | 449 | ----local s = _serialize (d.documentElement, {"hello, world"}) 450 | --local s = d.documentElement 451 | 452 | --print (s) 453 | 454 | --print "---" 455 | 456 | --print (pretty { 457 | -- nodeName = s[0], 458 | -- textcontent = s[1], 459 | -- attributes = s, 460 | -- nchild = #s, 461 | -- }) 462 | 463 | --print "---" 464 | ----print (pretty(s)) 465 | 466 | 467 | --for n in s:nextNode () do 468 | -- print ('',n[0]) 469 | --end 470 | --print "---" 471 | 472 | --local function filter (x,p) print (p) return true end 473 | 474 | --for n in s:nextNode (filter) do 475 | --end 476 | --print "---" 477 | -------------------------------------------------------------------------------- /tests/test_xml_html.lua: -------------------------------------------------------------------------------- 1 | local t = require "tests.luaunit" 2 | 3 | local pretty = require "pretty" 4 | 5 | -- XML module tests 6 | 7 | local x = require "openLuup.xml" 8 | local h = x.xhtml 9 | 10 | TestHTML = {} 11 | 12 | function TestHTML:test_html () 13 | 14 | local tab = h.table {style = "gratuitous"} 15 | tab.header {"one","two"} 16 | tab.row {{colspan=2, "wide"}} 17 | tab.row {{style= "name=bold", "bold"}, "normal"} 18 | local html = h.html { 19 | h.title "testHTML", 20 | h.meta {xmlns = "not sure what goes here"}, 21 | h.body { 22 | h.p "hello, world", 23 | tab 24 | }} 25 | 26 | print '' 27 | print (h.document (html)) 28 | 29 | print "-----" 30 | 31 | print(pretty(html)) 32 | 33 | end 34 | 35 | 36 | ------------------- 37 | 38 | if multifile then return end 39 | 40 | t.LuaUnit.run "-v" 41 | 42 | print ("TOTAL number of tests run = ", N) 43 | 44 | -- do return end 45 | ------------------- 46 | 47 | --------------------------------------------------------------------------------