├── runtests.sh ├── example1.lua ├── LICENSE ├── try_luaxp.lua ├── CHANGELOG.md ├── README.md ├── test ├── test.lua └── testdata.json └── luaxp.lua /runtests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | lua test/test.lua 4 | -------------------------------------------------------------------------------- /example1.lua: -------------------------------------------------------------------------------- 1 | luaxp = require('luaxp') 2 | 3 | local context = {} 4 | context.toradians = function( argv ) 5 | return argv[1] * math.pi / 180 6 | end 7 | 8 | print("The cosine of 45 degrees is " .. luaxp.evaluate("cos(toradians(45))", context)) 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE (LUAXP) 2 | 3 | LuaXP Copyright 2016,2018 Patrick H. Rigney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /try_luaxp.lua: -------------------------------------------------------------------------------- 1 | local luaxp = require "luaxp" 2 | local io = require "io" 3 | 4 | local r, s, v, m 5 | local ctx = { minrange = 0, maxrange=100, Device_Num_217={ status="OK", time=140394202, states={ { id="100", value="Hundred" }, { id="290", value="abc" } } } } 6 | 7 | io.write("Running with Luaxp version " .. luaxp._VERSION .. "\n") 8 | io.write("Context variables defined:\n") 9 | for r,s in pairs(ctx) do io.write(" " .. r .. "=" .. luaxp.dump(s) .. "\n") end 10 | io.write("\n") 11 | 12 | ctx.__functions = {} 13 | ctx.__functions.whiz = function(argv) return string.rep("*",argv[1]) end 14 | 15 | local function showErrorLocation( exprString, errStruct ) 16 | if errStruct and errStruct.location then 17 | io.write(exprString) 18 | io.write("\n") 19 | io.write(string.rep(" ", errStruct.location-1)) 20 | io.write("^\n") 21 | end 22 | end 23 | 24 | while (true) do 25 | io.write("\nEXP> ") 26 | io.flush() 27 | s = io.read() 28 | if (s == nil) then break end 29 | r, m = luaxp.compile(s) 30 | if (r == nil) then 31 | io.write("Expression parse failed: " .. luaxp.dump(m) .. "\n") 32 | showErrorLocation( s, m ) 33 | else 34 | if false then 35 | io.write("Compiled result: ") 36 | io.write(luaxp.dump(r)) 37 | io.write("\n") 38 | end 39 | v,m = luaxp.run(r, ctx) 40 | if (v == nil) then 41 | io.write("Expression evaluation failed: " .. luaxp.dump(m) .. "\n") 42 | showErrorLocation( s, m ) 43 | else 44 | io.write("Expression result: " .. ( luaxp.isNull(v) and "(null)" or luaxp.dump(v) ).."\n") 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LuaXP Change Log 2 | 3 | ## 1.0 4 | 5 | * Add `date(year[, month[ ,day[ ,hour[ ,min[ ,sec]]]]])` function to create datetime from parts as arguments. The current value is used for any part not provided (e.g. `date(null,1,1,0,0,0)` returns midnight Jan 1 of the current year. Trailing `null` arguments may be omitted (i.e. `date(2019,11,4)` is the same as `date(2019,11,4,null,null,null)`). Time is built in the current timezone. 6 | * Add `map(array[, expr[, varname]])`; similar to `iterate()`, it loops over the array, performing `expr` on each value, and builds a table with key as the original value, and the result of the expression as the value. For example, `map(list("dog","cat","fish"), _+" food")` returns a table `{ "dog"="dog food", "cat"="cat food", "fish"="fish food" }`. If either the value or the result of the expression is `null`, it is omitted from the result table. In the expression, the value is represented by the pseudo-variable `_` (underscore). If you give `varname` (third argument) to `map()`, that string will be used as the variable name instead. The special additional pseudo-variable `__` (two underscores) is populated with the index in the array of each value as processed, such that `map(list("dog","cat","fish"), __)` yields `{ "dog"=1, "fish"=3, "cat"=2 }`. If no expression is given, the default `__` (two underscores--the array index) is used (that is, `map(array)` is equivalent to `map(array, __)`). 7 | * The `timepart([timestamp[, utc]])` function, which returns the time parts (a table with keys year, month, day, hour, min, sec, isdst) for the given timestamp (optional, default current time/date), not accepts an optional `utc` argument, which when *true* returns the UTC parts rather than the local time parts. 8 | * POTENTIAL BREAKING CHANGE: LuaXP now attempts to load a `bit` (bitlib, etc.) module. If such a module can be loaded and contains the required functions, it is used; otherwise, the legacy internal implementation (weak, but wiggling) is used. Note that the results of these various implementations vary, so the user/integrator is advised to choose a library and be consistent. 9 | * POTENTIAL BREAKING CHANGE: The `__` name is now a reserved word. 10 | 11 | ## 0.9.9 12 | 13 | * Ignore whitespace between function name and paren in function refs (do same for array refs), so syntax isn't so strict. 14 | * Slight optimizations throughout. 15 | * Add Lua `..` string concatenation operator. 16 | * POTENTIAL BREAKING CHANGE: the "+" operator now does a better job identifying strings containing numbers as numbers, and handling the operation as addition rather than string concatenation. I doubt this will present any issues except for (hopefully rare) cases where you actually want `"123"+"456"` to equal "123456". The `..` operator (Lua string concatentation) has been added for deterministic handling as strings (e.g. `'123'..'456'` will always produce the string '123456' and not the number 579). 17 | 18 | ## 0.9.8 19 | 20 | * Implement split( string, sep ) function; splits string to array on sep (e.g. split( "1,2,3", ",") returns array [1,2,3]). Sep is a Lua pattern, so special chars must be escaped in the Lua pattern way (e.g. to use "+" as a separator, must specify "%+"). 21 | * Implement join( array, sep ) function; joins array elements with sep into string result (e.g. join( list( 1,2,3 ), ":") result is "1:2:3" ). 22 | 23 | ## 0.9.7 24 | 25 | * Support deferred evaluation for if() and iterate(), and logical operators (&&/and, ||/or) 26 | * Change license to MIT License (previously GPL3) 27 | 28 | ## 0.9.4 29 | 30 | * Numerous bug fixes. 31 | * Test suite. 32 | * Iteration, lists (arrays), assignments (issue #3). 33 | * Improve error reporting (issue #2). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luaxp 2 | Luaxp is a simple arithmetic expression parser for Lua. 3 | 4 | Luaxp supports simple mathemtical expressions for addition, subtraction, multiplication, 5 | division, modulus, bitwise operations, and logical operations. It has a small library of 6 | built-in functions (abs, cos, sin, floor, ceil, round, etc.). 7 | 8 | Through a passed-in context table, Luaxp supports named variables, and custom functions. 9 | See the documentation below for how to implement these. 10 | 11 | Luaxp is offered under MIT License as of October 29, 2018 (beginning with version 0.9.7). 12 | 13 | ## Github 14 | 15 | There are three branches in the Github repository: 16 | * master - The current released version; this is the version to use/track if you are incorporating LuaXP into other projects; 17 | * develop - The current development version, which may contain work in progress, partial implementations, debugging code, etc. ("the bleeding edge"); 18 | * stable - The current stable development code, which contains only completed and tested functionality, but may still contain debug messages and lack some optimizations and refinement. 19 | 20 | Code moves from the develop branch to the stable branch to the master branch. There is no release schedule. Releases are done as needed. 21 | 22 | ## Installation ## 23 | 24 | Some day. This is all very new. 25 | 26 | Grab it. Put in your shared Lua directory (`/usr/share/lua/...?`) or keep it where you use it. Try out the 27 | free-form test program `try_luaxp.lua`. This lets you enter expressions and see the results. 28 | 29 | TO-DO: Install with LuaRocks 30 | 31 | ## Known Issues ## 32 | 33 | As of version 0.9.7, the following are known issues or enhancement that are currently being considered: 34 | 35 | None 36 | 37 | ## Bug Reports and Contributions ## 38 | 39 | I wrote this library as a port of a similar 40 | library I wrote for JavaScript called lexp.js (Lightweight Expression Parser). It differs slightly 41 | in operation, but the underlying approach is fundamentally the same and it's a very close port. I 42 | did this mainly for fun. I use lexp.js in a dashboard system that I wrote (I wanted 43 | something simpler to set up and 44 | manage than dashing, which is great, but has way too high a setup and learning curve, but I digress), 45 | and figured that somebody might make use of it in Lua as well. 46 | 47 | I like bug reports. I like help. I like making things better. If you have suggestions or bug reports 48 | please use use [GitHub Issues](https://github.com/toggledbits/luaxp/issues). If you have a 49 | contribution, have at it! Please try to follow the coding style to keep it consistent, and use spaces 50 | rather than tabs (4 space indenting). 51 | 52 | Also, if you're making a feature enhancement contribution, consider looking at [my lexp project](https://github.com/toggledbits/lexpjs) as well, 53 | and see if the same enhancement would be appropriate there. Since the Lua implementation is born of the 54 | JavaScript one, I think it would be an interesting exercise to try and keep them as close functionally 55 | as possible. 56 | 57 | ## Syntax ## 58 | 59 | This is a very rough BNF for the parser: 60 | 61 | ``` 62 | ::= 63 | | 64 | | 65 | | "[" "]" 66 | | "(" ")" 67 | | 68 | | 69 | | "(" ")" 70 | 71 | ::= "" | 72 | 73 | ::= [ "," ] 74 | 75 | ::= "-" | "+" | "!" 76 | 77 | ::= "+" | "-" | "*" | "/" | "%" 78 | | "&" | "|" | "^" 79 | | "<" | "<=" | ">" | ">=" | "==" | "=" | "<>" | "!=" 80 | 81 | :== | /* must eval to number */ 82 | 83 | ::= 84 | | "0x" 85 | | "0b" 86 | | "0" 87 | | 88 | 89 | ::= "'" "'" 90 | | '"' '"' 91 | 92 | ::= { | | "_" | "." } 93 | 94 | ::= { | | "_" } 95 | ``` 96 | 97 | This is intentionally simplified and doesn't exhaustively convey the full syntax, which would be too detailed to convey the concept quickly. Specific elements of the syntax such are array and dot notation for traversal of trees/structures is not shown (e.g. expressions forms "weather.current" and "weather['current'], which are equivalent). 98 | 99 | ## The Basics ## 100 | 101 | To load the library, use a `require()` statement: 102 | 103 | ``` 104 | luaxp = require "luaxp" 105 | ``` 106 | 107 | ### compile( expressionString ) ### 108 | 109 | The `compile()` function accepts a single argument, the string the containing the expression to be parsed. 110 | If parsing of the expression succeeds, the function returns a table containing the parse tree that is used 111 | as input to `run()` later. If parsing fails, the function returns two values: `nil` and a table containing information about the error. 112 | 113 | Example: 114 | 115 | ``` 116 | luaxp = require('luaxp') 117 | 118 | local parsedExp,err = luaxp.compile("abs(355/113-pi)") 119 | if parsedExp == nil then 120 | -- Parsing failed 121 | print("Expression parsing failed. Reason: " .. luaxp.dump(err)) 122 | else 123 | -- Parsing succeeded, on to other work... 124 | ... 125 | end 126 | ``` 127 | 128 | This example uses the LuaXP public function `dump()` to display the contents of the `err` table returned. 129 | 130 | ### run( parsedExp [, executionContext ] ) ### 131 | 132 | The `run()` function executes the parsed expression. It takes an optional `executionContext` argument, which 133 | is a table containing variable names and functions. 134 | 135 | `run()` returns the result of the expression evaluation. If the evaluation succeeds, the first return value will always be non-`nil`. If it fails, two values are returned: `nil` and a string containing the 136 | error message (i.e. same semantics as `compile()`). You should always check for evaluation errors, as these are errors that were not or could not be detected in parsing (e.g. a sub-expression used as a divisor evaluates to zero, thus an attempt to divide by zero). 137 | 138 | ``` 139 | luaxp = require "luaxp" 140 | 141 | local parsedExp, cerr = luaxp.compile("abs(355 / 113 - pi)" ) 142 | if parsedExp == nil then error("Parsing failed: " .. cerr.message) end 143 | 144 | local context = { pi = math.pi } 145 | 146 | local resultValue, rerr = luaxp.run( parsedExp, context ) 147 | if resultValue == nil then 148 | error("Evaluation failed: " .. rerr.message) 149 | else 150 | print("Result:", luaxp.isNull(resultValue) and "NULL" or tostring(resultValue) ) 151 | end 152 | ``` 153 | 154 | In the above example, a context is created to define the value of "pi" that is used in the parsed expression. 155 | This context is then passed to `run()`, which uses it to dereference the value on the fly. 156 | 157 | The code also checks the return value for the special "null" value. If the result of an expression results in "no value", LuaXP does not use Lua `nil`, it has its own indicator, and your code should check for this as shown above. 158 | 159 | As of this version, Luaxp does not allow you to modify variables or create new ones during evaluation. 160 | 161 | ### evaluate( expressionString [, executionContext ] ) ### 162 | 163 | The `evaluate()` function performs the work of `compile()` and `run()` in one step. The function result 164 | is the value of the parsed and evaluated expression, unless a parsing or evaluation error occurs, in which 165 | case the function will return two values: `nil` and an error message. 166 | 167 | ``` 168 | luaxp = require "luaxp" 169 | 170 | local context = { pi = math.pi } 171 | local resultValue,err = luaxp.evaluate("abs(355/113-pi)", context) 172 | if resultValue == nil then 173 | error("Error in evaluation of expression: " .. err.message) 174 | else 175 | print("The difference between the two approximations of pi is " .. tostring(result)) 176 | end 177 | ``` 178 | 179 | ### Other Functions and Values 180 | 181 | The LuaXP `dump()` function will return a string containing a safely-printable representation of the passed value. If the value passed is a table, for example, `dump()` will display it in a Lua-like table initialization syntax (tuned for readability, not for re-use as actual Lua code). 182 | 183 | The `isNull()` function returns a boolean indicating if the passed argument is LuaXP's null value. 184 | 185 | The `null` and `NULL` constants (synonyms) are the represtations of LuaXP's null value. Thus the test `returnValue==luaxp.null` in Lua is equivalent to `isNull(returnvalue)`. The constants can also be used to initialize values when creating the execution context. 186 | 187 | ### Reserved Words 188 | 189 | The words `true` and `false` are reserved and evaluate to their respective boolean values. The words `null`, `NULL`, and `nil` evaluate to the LuaXP null value. 190 | 191 | The reserved words `pi` and `PI` (synonyms) are provided as a convenience and evaluate to the underyling Lua Math library implementation of `math.pi`. 192 | 193 | ### Error Returns 194 | 195 | If a LuaXP call results in an error (`nil` first return value), the error table (second return value) contains the following elements: 196 | * `type` - Always included, the string "compile" or "evaluation" to indicate the stage at which the error was detected. 197 | * `message` - Always included, text describing the error. 198 | * `location` - Sometimes included, the character position at which the error was detected, if available. 199 | 200 | The _try_luaxp.lua_ example included with LuaXP shows how the `location` value can be used to provide feedback to the user when errors occur. Try entering "0b2" and "max(1,2,nosuchname)" into this example program. 201 | 202 | ## Context Variables ## 203 | 204 | The context passed to `evaluate()` and `run()` is used to define named variables and custom functions 205 | that can be used in expressions. We've seen in the above examples for these functions how that works. 206 | For variables, it's simple a matter of defining a table element with the value to be used: 207 | 208 | ``` 209 | local context = {} 210 | context.minrange = 0 211 | context.maxrange = 100 212 | 213 | -- or the more streamlined: 214 | 215 | local context = { minrange=0, maxrange=100 } 216 | ``` 217 | 218 | These may be referred to in expressions simply by their names as defined (case sensitive): 219 | 220 | ``` 221 | $ lua try_luaxp.lua 222 | Running with Luaxp version 0.9.2 223 | Context variables defined: 224 | minrange=0 225 | maxrange=100 226 | 227 | EXP> maxrange 228 | Expression result: 100 229 | 230 | EXP> (maxrange-minrange)/2 231 | Expression result: 50 232 | 233 | EXP> nonsense 234 | Expression evaluation failed: Undefined variable: nonsense 235 | ``` 236 | 237 | Variables can also use dotted notation to traverse a tree of values in the context: 238 | 239 | ``` 240 | context.device = {} 241 | context.device.class = "motor" 242 | context.device.info = { location="MR1-15-C02", specs={ manufacturer="Danfoss", model="EM5-18-184T", frame="T", voltage="460", hp="5" } } 243 | ``` 244 | 245 | In expressions, the value `device.class` would therefore be *motor*. Referring simply to `device`, however, would return a runtime 246 | evaluation error. 247 | 248 | The second more complex example shows that dotted notation can be used to traverse more deeply-nested structure. In this example, 249 | one could derive the horsepower of the example motor by referring to `device.info.specs.hp`. 250 | 251 | ## Custom Functions ## 252 | 253 | You can define custom functions for your expressions by defining them in the context passed to `run()` or 254 | `evaluate()`. 255 | 256 | It's pretty straightforward to do. Your custom function must be implemented by a Lua function that takes a 257 | single argument, which I'll call `argv` simply for example purposes. This is an array of the expression values 258 | parsed. 259 | 260 | Let's say we want to create a function to convert degrees to radians. The math for that is pretty easy. 261 | It's the value in degrees times "pi" and divided by 180. If you wrote that just as a plain Lua function, 262 | it would probably look something like this: 263 | 264 | ``` 265 | function toRadians(degrees) 266 | return degrees * math.pi / 180 267 | end 268 | ``` 269 | 270 | To make that a function that your expressions could use, you need to put it into the context that's passed 271 | to `run()`, which is done like this: 272 | 273 | ``` 274 | local context = {} 275 | context.toradians = function( argv ) 276 | return args[1] * math.pi / 180 277 | end 278 | ``` 279 | 280 | Now, when you run your expression, you can pass this context, and the evaluator will know what `toradians` 281 | means in the expression: 282 | 283 | ``` 284 | luaxp = require "luaxp" 285 | 286 | local context = {} 287 | context.toradians = function( argv ) 288 | return argv[1] * math.pi / 180 289 | end 290 | 291 | print("The cosine of 45 degrees is " .. luaxp.evaluate("cos(toradians(45))", context)) 292 | ``` 293 | 294 | Although we have used an anonymous function in this example, there is no reason you could not separately 295 | define a named function, and simply use a reference to the function name in the context assignment, like 296 | this: 297 | 298 | ``` 299 | function toRadians(argv) 300 | return argv[1] * math.pi / 180 301 | end 302 | context.toradians = toRadians 303 | ``` 304 | 305 | The premise here is simple, if it's not already clear enough. The evaluator will simply look in your passed 306 | context for any name that it doesn't recognize as one of its predefined functions. 307 | If it finds a table element with a 308 | key equal to the name, the value is assumed to be a function it can call. The function is called with a 309 | single argument, a table (as an array) containing all of the arguments that were parsed in the expression. 310 | There is no limit to the number of arguments. Your function is responsible for sanity-checking the number 311 | of arguments, their values/type, and supplying defaults if necessary. 312 | 313 | Note in the above example that we defined our function with an uppercase letter "R" in the name, 314 | but when we made the context assignment, the context element has all lower case. This means that 315 | any expression would also need to use all lower case. The name used in evaluation is the name on 316 | the context element, not the actual name of the function. 317 | 318 | If we run our program (which is available as `example1.lua` in the repository), here's the output: 319 | 320 | ``` 321 | $ lua example1.lua 322 | The cosine of 45 degrees is 0.70710678118655 323 | ``` 324 | 325 | ## "Local" Variables 326 | 327 | The evaluator supports assignment of a value to local variable. If multiple expressions are evaluated using the same context, the local variables defined by earlier expressions are visible to the later ones. 328 | 329 | ``` 330 | luaxp = require "luaxp" 331 | 332 | ctx = {} 333 | result = luaxp.evaluate( "v=100", ctx ) 334 | print(result) -- prints 100 335 | 336 | result = luaxp.evaluate( "v=v*2", ctx ) 337 | print(result) -- prints 200 338 | 339 | result = luaxp.evaluate( "v/5", ctx ) 340 | print(result) -- prints 40 341 | ``` 342 | 343 | The local variables accumulated in the context are stored under the `__lvars` key. Thus, in this example, `ctx.__lvars.v` would be defined and have the value 40 in Lua after all three evaluations. 344 | 345 | Local variables are in scope before context variables (that is, if a local variable has the same name as a context variable, the local variable will always take precedence): 346 | 347 | ``` 348 | luaxp = require "luaxp" 349 | 350 | ctx = {} 351 | ctx.alpha = 57 -- context variable definition 352 | result = luaxp.evaluate( "alpha", ctx ) 353 | print(result) -- prints 57 as expected 354 | 355 | -- This expression creates a local variable with the same name 356 | luaxp.evaluate( "alpha=99", ctx ) 357 | 358 | -- Now that we've set local alpha, we can't "see" the context variable 359 | result = luaxp.evaluate( "alpha", ctx ) 360 | print(result) -- prints 99 361 | 362 | -- If we print what's in the context, we see two different values, one 363 | -- for the original context variable as we defined it, the other for 364 | -- the local variable defined by the expression evaluation. 365 | print(ctx.alpha) -- prints 57 366 | print(ctx.__lvars.alpha) -- prints 99 367 | ``` -------------------------------------------------------------------------------- /test/test.lua: -------------------------------------------------------------------------------- 1 | -- Find the luaxp we're going to test 2 | local moduleName = arg[1] or "luaxp" 3 | local L = require(moduleName) 4 | local json = require "dkjson" 5 | 6 | -- FOCUSTEST, when set, debug on for specific test number 7 | FOCUSTEST = 0 8 | 9 | -- Load Test Data 10 | local testData = arg[2] or "test/testdata.json" 11 | 12 | local mt = getmetatable(_G) 13 | if mt == nil then 14 | mt = {} 15 | setmetatable(_G, mt) 16 | end 17 | 18 | __STRICT = true 19 | mt.__declared = {} 20 | 21 | mt.__newindex = function (t, n, v) 22 | if __STRICT and not mt.__declared[n] then 23 | local w = debug.getinfo(2, "S").what 24 | if w ~= "C" then 25 | print("ASSIGNMENT TO GLOBAL "..n) 26 | -- error("assign to undeclared global variable '"..n.."'", 2) 27 | end 28 | mt.__declared[n] = true 29 | end 30 | rawset(t, n, v) 31 | end 32 | 33 | mt.__index = function (t, n) 34 | if not mt.__declared[n] and debug.getinfo(2, "S").what ~= "C" then 35 | print("REFERENCE TO UNDECLARED GLOBAL " .. n) 36 | print(debug.traceback()) 37 | error("variable '"..n.."' is not declared", 2) 38 | end 39 | return rawget(t, n) 40 | end 41 | 42 | local function debugPrint( msg ) 43 | print(string.char(27) .. "[0;34;40m" .. msg .. string.char(27) .. "[0m") -- debug in blue 44 | end 45 | -- Uncomment the line below to enable debugging 46 | --L._DEBUG = debugPrint 47 | 48 | local ctx = {} 49 | local nTest = 0 50 | local nErr = 0 51 | local nSkip = 0 52 | 53 | local RED = string.char(27) .. "[0;31;40m" 54 | local YELLOW = string.char(27) .. "[0;33;40m" 55 | local RESET = string.char(27) .. "[0m" 56 | 57 | local function fail(m, ...) 58 | local msg 59 | if m == nil then msg = "Incorrect result, check manually" 60 | else msg = string.format(m, ...) end 61 | print(RED .. " >>>>> FAIL, " .. msg .. RESET) 62 | nErr = nErr + 1 63 | end 64 | 65 | local function skip(s, ...) 66 | nTest = nTest + 1 67 | print(string.format("%03d: %s", nTest, s)) 68 | print(string.format(YELLOW .. " ***** SKIPPED, ", nTest) .. string.format(...) .. RESET) 69 | nSkip = nSkip + 1 70 | end 71 | 72 | --[[ Evaluate the passed string s. Compare to the expected result. 73 | To pass, the result must have the same value and data type as 74 | expected. If the expression is meant to throw an error, then 75 | failExpect may contain a fragment of the expected error message, 76 | and it is a failure for the expression to not fail or fail with 77 | any other message. 78 | --]] 79 | local function eval(s, expected, failExpect, comment, ...) 80 | local pdebug 81 | nTest = nTest + 1 82 | if nTest == FOCUSTEST then pdebug = L._DEBUG ; L._DEBUG = debugPrint end 83 | local r,err = L.evaluate(s, ctx) 84 | L._DEBUG = pdebug 85 | local mm, errmsg 86 | if r == nil then 87 | -- There was an error 88 | if type(err) == "table" then 89 | mm = string.format("(%s error at %s) %s", err.type, err.location or "unknown", err.message) 90 | errmsg = err.message 91 | else 92 | mm = string.format("(RUNTIME ERROR) %s", tostring(err)) 93 | errmsg = err 94 | end 95 | elseif r == L.NULL then 96 | mm = string.format("(luaxp)NULL") 97 | else 98 | mm = string.format("(%s)%s", type(r), L.dump(r)) 99 | end 100 | print(string.format("%03d: %s=%s", nTest, s, mm)) 101 | if comment ~= nil then 102 | print(" NOTE: " .. comment) 103 | end 104 | if r == nil then 105 | if failExpect == nil or not string.find( errmsg, failExpect ) then 106 | -- If you get here and you think your string matches, you may need to escape special (pattern) chars 107 | fail("unexpected error thrown: %s (%s)", mm, failExpect) 108 | end 109 | elseif failExpect ~= nil then 110 | fail("expected error not thrown (%s)", failExpect) 111 | elseif expected ~= nil then 112 | if type(expected) == "function" then 113 | expected( r, ... ) 114 | elseif type(r) == type(expected) then 115 | if type(r) == "number" then 116 | local delta = r - expected 117 | if math.abs(delta) > 0.00001 then 118 | fail("expected (%s)%s, delta %f", type(expected), tostring(expected), delta) 119 | end 120 | elseif type(r) == "table" then 121 | for k,v in pairs(expected) do 122 | if r[k] == nil or r[k] ~= v then 123 | fail("expected (%s)%s, missing %s", type(expected), dump(expected), tostring(k)) 124 | end 125 | end 126 | else 127 | if r ~= expected then 128 | fail("expected (%s)%s", type(expected), tostring(expected)) 129 | end 130 | end 131 | else 132 | fail("expected (%s)%s", type(expected), tostring(expected)) 133 | end 134 | else 135 | print(YELLOW .. " !!!!! WARNING, test has no defined expected result; check manually." .. RESET) 136 | end 137 | return r 138 | end 139 | 140 | -- ********************* TIME PARSING TESTS ********************** 141 | --[[ 142 | NOTA BENE! These tests were largely written to run on a system configured 143 | for the America/New York time zone. If run in a different zone, 144 | errors would be expected and adjustments would need to be made. 145 | --]] 146 | local function doTimeTests() 147 | local now = eval("time()", function( result ) if result ~= os.time() then fail() end end) 148 | local localeDateTime = eval("strftime(\"%x %X\", " .. now .. ")", nil, nil, "The result should be current date in locale format") 149 | eval("strftime(\"%b %B\", time())", nil, nil, "The result should be abbrev and full name for current month in locale language/format") 150 | eval("time('2014-04-28T16:00-04:00')", 1398715200) 151 | eval("time('2017-01-20T12:00:00-05:00')", 1484931600) 152 | eval("time('2013-07-08T09:10:00.553-04:00')", 1373289000) 153 | eval("time('2019-03-26T09:29:00-0000')", 1553592540) 154 | eval("time('2019-05-01T10:00:00+0100')", 1556701200) 155 | eval("time('20180128T151617-0500')", 1517170577) 156 | eval("time('2013-07-13')", 1373688000) 157 | eval("time('12/21/2021T0000')", 1640062800) 158 | eval("time('8/8/2008 8:8:8')", 1218197288) 159 | eval("time('7/7/7 7:7:7')", 1183806427) 160 | eval("time('13/11/2011')", 1321160400) 161 | eval("time('12:45')", function( result ) local dn = os.date("*t") local dr = os.date("*t", result) if not (dr.hour == 12 and dr.min == 45 and dr.year == dn.year and dr.month == dn.month and dr.day == dn.day and dr.sec == 0 ) then fail() end end) 162 | eval("time('0300')", function( result ) local dn = os.date("*t") local dr = os.date("*t", result) if not (dr.hour == 3 and dr.min == 0 and dr.year == dn.year and dr.month == dn.month and dr.day == dn.day and dr.sec == 0 ) then fail() end end) 163 | eval("time('8/1 1:45pm')", function( result ) local dn = os.date("*t") local dr = os.date("*t", result) if not (dr.hour == 13 and dr.min == 45 and dr.year == dn.year and dr.month == 8 and dr.day == 1 and dr.sec == 0 ) then fail() end end) 164 | eval("time('8/1/15 3:17am')", 1438413420) 165 | eval("time('Mon Jan 29 9:43:00 2018')", 1517236980) 166 | eval("time('Jan 31 2018')", 1517374800) 167 | eval("time('Jul 4 09:43p')", function( result ) local dn = os.date("*t") local dr = os.date("*t", result) if not (dr.hour == 21 and dr.min == 43 and dr.year == dn.year and dr.month == 7 and dr.day == 4 and dr.sec == 0 ) then fail() end end) 168 | eval("time('10-Nov-2018')", 1541826000) 169 | eval("time('" .. localeDateTime .. "')", now) 170 | eval("time('Mar 10 2014 +24:00:00')", 1394510400) 171 | eval("time('Oct 1 2009 +30:00:00:00')", 1256961600) 172 | eval("time('Sep 21 2012 15:30 -12:15')", 1348255065) 173 | eval("time('13/11/2011 garbage-at-end')", 1321142400, "Unparseable data") 174 | local thn = eval("dateadd('2018-06-15', 45, 30, 15, 6, 3, 2)", 1600716645) 175 | eval("datediff(dateadd(time(),0,0,0,1))", 86400) 176 | eval("dateadd('1980-09-01',0,0,0,0,360)", 1283313600) 177 | -- Test date() function, builds date from y,m,d,h,m,s arguments; any null=current 178 | eval("date(2019,11,4,15,5,0)", 1572897900,nil,"Matched result assumes local TZ is America/New York") 179 | eval("strftime('%c', date(null,null,null,6,30,0))", nil, nil, "Result should be today 6:30am local time") 180 | eval("strftime('%c', date(2020))", nil, nil, "Result should be year 2020 today's month, day, and current time") 181 | eval("strftime('%c', date(null,1,1,0,0,0))", nil, nil, "Result is midnight Jan 1 of this year") 182 | 183 | if ctx.response ~= nil then 184 | eval("strftime(\"%c\", response.loadtime)", nil, nil, "The result should comport with the loadtime value in sample.json") 185 | else 186 | skip("strftime(\"%c\", response.loadtime)", "file sample1.json could not be loaded") 187 | end 188 | end 189 | 190 | local function doStringOpsTests() 191 | eval('"Es gibt kein Bier auf Hawaii"', "Es gibt kein Bier auf Hawaii") 192 | eval("'Ich bin Berliner!'", "Ich bin Berliner!") 193 | eval("'Hello \"there\"'", 'Hello "there"') 194 | eval('"Well, \'hello\' to you too"', "Well, 'hello' to you too") 195 | eval("'collaborate' + 'learn'", "collaboratelearn") 196 | eval("'abc'-'def'", nil, "string to number failed") 197 | eval("'abc'*'def'", nil, "string to number failed") 198 | eval("'*'*20", nil, "string to number failed") 199 | eval("'abc'/'def'", nil, "string to number failed") 200 | eval("99 + ' bottles of beer on the wall'", "99 bottles of beer on the wall") 201 | eval("'There are ' + 0 + ' remaining.'", "There are 0 remaining.") 202 | eval('"What is the reason?', nil, "Unterminated string") 203 | eval("'New York' == 'NEW YORK'", false) 204 | eval("'New York' == 'New York'", true) 205 | eval("'New York' == 'Philadelphia'", false) 206 | eval("'New York' != 'NEW YORK'", true) 207 | eval("'New York' != 'New York'", false) 208 | eval("'New York' != 'Philadelphia'", true) 209 | end 210 | 211 | local function doNumericParsingTests() 212 | eval("0",0) 213 | eval("1",1) 214 | eval("-1",-1) 215 | eval("-1+1",0) 216 | eval("186282",186282) 217 | eval("-255",-255) 218 | eval("077",63) 219 | eval("0x1F",31) 220 | eval("0b0011",3) 221 | eval("1e3", 1000) 222 | eval("1E", nil, "Missing exponent") 223 | eval("10e-1",1) 224 | eval("-0.567112E+06", -567112) 225 | eval(".7177", 0.7177) 226 | eval("'123'+321", 444) 227 | eval("tonumber(123)+321",444) 228 | eval("pi",3.14159265) 229 | eval("0xgg", nil, "Invalid") 230 | eval("0ff", nil, "Invalid") 231 | eval("0b2", nil, "Invalid") 232 | end 233 | 234 | local function doNumericOpsTests() 235 | eval("123 + 456", 579) 236 | eval("579-123", 456) 237 | eval("8--1", 9) 238 | eval("-8-1", -9) 239 | eval("-8--9", 1) 240 | eval("123*4", 492) 241 | eval("492/123", 4) 242 | eval("127 % 100", 27) 243 | eval("400 % 100", 0) 244 | eval("300 < 400", true) 245 | eval("300 < 300", false) 246 | eval("300 < 200", false) 247 | eval("500 > 100", true) 248 | eval("500 > 500", false) 249 | eval("500 > 600", false) 250 | eval("300 <= 400", true) 251 | eval("300 <= 300", true) 252 | eval("300 <= 200", false) 253 | eval("500 >= 100", true) 254 | eval("500 >= 500", true) 255 | eval("500 >= 600", false) 256 | eval("500 == 500", true) 257 | eval("500 != 500", false) 258 | eval("500 == 600", false) 259 | eval("500 != 600", true) 260 | eval("15&8",8) 261 | eval("7&8",0) 262 | eval("2|4",6) 263 | eval("6^4",2) 264 | eval("!8", -9) 265 | eval("!0", -1) 266 | 267 | -- Precedence tests 268 | eval("1+2*3", 7) 269 | eval("1*2-4", -2) 270 | eval("8-32/4", 0) 271 | eval("1+(2-4)", -1) 272 | eval("(((((((((( 24 ))))))))))", 24) 273 | eval("2+4>5", true) 274 | eval("2+6!=6", true) 275 | eval("1==1&4", true, nil, "Equiv (1==1)&4 so true&4 is true; not 1==(1&4), 1==0 is false") 276 | eval("1==(1&4)", false) 277 | eval("1+1&5", 0, nil, "Equiv (1+1)&5 so 2&5 is 0; not 1+(1&5), 1+1 is 2") 278 | eval("1+(1&5)", 2) 279 | eval("3|4+7", 11, nil, "Equiv 3|(4+7), 3|11 is 11; not (3|4)+7, 7+7 is 14") 280 | eval("(3|4)+7", 14) 281 | end 282 | 283 | local function doBooleanOpsTests() 284 | -- Note !0 and !1 are treated as number, not boolean, and produce a 32-bit bitwise result. See num ops tests 285 | eval("true", true) 286 | eval("false", false) 287 | eval("!'0'",true) 288 | eval("!'1'",false) 289 | eval("true&1",true) 290 | eval("true&0",false) 291 | eval("true&'true'", true) 292 | eval("'false'&'false'", false) 293 | eval("'false'&'true'", false) 294 | eval("'true'&'true'", true) 295 | eval("'false'|'false'", false) 296 | eval("'false'|'true'", true) 297 | eval("'true'|'true'", true) 298 | eval("'false'^'false'", false) 299 | eval("'false'^'true'", true) 300 | eval("'true'^'true'", false) 301 | eval("true && true", true) 302 | eval("true && false", false) 303 | eval("false && true", false) 304 | eval("true || true", true) 305 | eval("true || false", true) 306 | eval("false || false", false) 307 | eval("true and true", true) 308 | eval("true and false", false) 309 | eval("false and true", false) 310 | eval("true or true", true) 311 | eval("true or false", true) 312 | eval("false or false", false) 313 | eval("true and true or false", true) 314 | eval("true and false or false", false) 315 | eval("true and 'yes' or 'no'", "yes") 316 | eval("false and 'yes' or 'no'", "no") 317 | end 318 | 319 | local function doMathFuncTests() 320 | eval("abs(123)", 123) 321 | eval("abs(-123)", 123) 322 | eval("abs(0)", 0) 323 | eval("abs(-0)", 0) 324 | eval("sgn(123)", 1) 325 | eval("sgn(-123)", -1) 326 | eval("sgn(0)", 0) 327 | eval("sgn(-0)", 0) 328 | eval("round(1.1111,2)", 1.11) 329 | eval("round(1.78,0)", 2) 330 | eval("round(0, 4)", 0) 331 | eval("sqrt(64)",8) 332 | eval("sin(pi/2)",1) 333 | eval("cos(pi/2)",0) 334 | eval("sin(0)",0) 335 | eval("cos(0)",1) 336 | eval("sqrt(2)/2") 337 | eval("acos(1)", 0) 338 | eval("asin(0)", 0) 339 | eval("atan(pi)") 340 | eval("cos('wilma')", nil, "Non%-numeric argument 1") 341 | eval("sin(45 * pi / 180)", nil, nil, "The result should be about sqrt(2)/2 = 0.707...") 342 | eval("floor(123)",123) 343 | eval("floor(0.123*1000)", 123.0, nil, "There is a known Lua rounding error here, but == comparisons on floats are always dangerous in any language") 344 | eval("floor(1.8)", 1) 345 | eval("floor(1.2)", 1) 346 | eval("floor(-1.2)", -2) 347 | eval("ceil(1.8)", 2) 348 | eval("ceil(1.2)", 2) 349 | eval("ceil(-1.2)", -1) 350 | eval("pow(10,2)", 100) 351 | eval("pow(10,-1)", 0.1) 352 | eval("min(1,9)", 1) 353 | eval("min(9,1)", 1) 354 | eval("max(1,9)", 9) 355 | eval("max(9,1)", 9) 356 | eval("array=list(5,16,2,6,-1,15,12)", nil, nil, "Array setup for next test") 357 | eval("min(1,2,3,array)", -1) 358 | eval("max(1,2,3,array)", 16) 359 | eval("randomseed(123)", 123) 360 | eval("randomseed()", os.time(), nil, "May be slightly off sometimes") 361 | eval("random()", nil, nil, "Random number between 0 and 1") 362 | eval("random(100)", nil, nil, "Random number between 1 and 100") 363 | eval("random(20,30)", nil, nil, "Random number between 20 and 30") 364 | end 365 | 366 | local function doStringFuncTests() 367 | eval("(123)..(456)", "123456") 368 | eval("'abc'..987", "abc987") 369 | eval("'abc'+987", "abc987") 370 | eval("45+'°C'", "45°C") 371 | eval("len('The rain in Spain stays mainly in the plain.')", 44) 372 | eval("sub('The rain in Spain stays mainly in the plain.', 5, 8)", "rain") 373 | eval("sub('The rain in Spain stays mainly in the plain.', 40, 49)", "lain.") 374 | eval("sub('The rain in Spain stays mainly in the plain.', 35, -5)", "the pl") 375 | eval("sub('The rain in Spain stays mainly in the plain.', 39)", "plain.") 376 | eval("sub('[in brackets]', 2, -2)", "in brackets") 377 | eval("find('The rain in Spain stays mainly in the plain.', 'not there')", 0) 378 | eval("find('The rain in Spain stays mainly in the plain.', 'plain.')", 39) 379 | eval("upper('The rain in Spain stays mainly in the plain.')", "THE RAIN IN SPAIN STAYS MAINLY IN THE PLAIN.") 380 | eval("lower('The rain in Spain stays mainly in the plain.')", "the rain in spain stays mainly in the plain.") 381 | eval("format('I like %s, I buy %dkg at a time.', 'cheese', 5)", "I like cheese, I buy 5kg at a time.") 382 | eval("rtrim(' only on the right ')", " only on the right") 383 | eval("ltrim(' only on the left ')", "only on the left ") 384 | eval("trim(' both sides ')", "both sides") 385 | eval("tostring(true)", "true") 386 | eval("tostring(123)", "123") 387 | eval("tostring(1.23)", "1.23") 388 | eval("tostring('cardiovascular')", "cardiovascular") 389 | eval("tonumber(true)", 1) 390 | eval("tonumber(false)", 0) 391 | eval("tonumber(123)", 123) 392 | eval("tonumber(12.3)", 12.3) 393 | eval("tonumber('456')", 456) 394 | eval("tonumber('1e5')", 100000) 395 | eval("tonumber('dog and cat')", nil, "could not be converted") 396 | eval("tonumber('1E',16)", 30) 397 | eval("tonumber('-7f',16)", nil, nil, "Known limitation in Lua tonumber(), ignore this case.") 398 | eval("tonumber('377',8)", 255) 399 | eval("tonumber('-377',8)", nil, nil, "Known limitation in Lua tonumber(), ignore this case.") 400 | eval("tonumber('1001',2)", 9) 401 | eval("split('A,B,C,D,E', ',')", nil, nil, "Array of 5 elements") 402 | eval("split('F,G,H')", nil, nil, "Array of 3 elements") 403 | eval("join(split('Z+Y+X+W+V+U', '%+'), 'M')", "ZMYMXMWMVMU") 404 | end 405 | 406 | local function doMiscFuncTests() 407 | eval("if(1==1,\"true\",\"false\")", "true") 408 | eval("if(7==8,\"true\",\"false\")", "false") 409 | eval("if(null,\"true\",\"false\")", "false") 410 | eval("if(7==8,\"true\")", L.NULL) 411 | eval("if(1==1,null,123)", L.NULL) 412 | 413 | eval("choose(3,\"default\",\"A\",\"B\",\"C\",\"D\")", "C") 414 | eval("choose(9,\"default\",\"A\",\"B\",\"C\",\"D\")", "default") 415 | 416 | eval("#list(1,2,3,4,5,9)", 6, nil, "Returns table of six elements") 417 | eval("list(time(),strftime('%c',time()))", nil, nil, "Returns two-element array with timestamp and string time") 418 | eval("first(list('dog','cat','mouse',time(),upper('leaf')))", "dog") 419 | eval("first(list())", L.NULL, nil, "First element of empty list returns null") 420 | eval("last(list('dog','cat','mouse',time(),upper('leaf')))", "LEAF") 421 | eval("last(list())", L.NULL, nil, "Last element of empty list returns null") 422 | eval("last('cat')", L.NULL, nil, "Invalid data returns null", "Test constant") 423 | eval("last(tonumber('123'))", L.NULL, nil, "Invalid data returns null", "Test expression") 424 | 425 | if ctx.response ~= nil then 426 | eval("#keys(response.rooms)", 23) 427 | eval("i=''", "",nil,"Setup for next test") 428 | eval("iterate(list(1,2,3,4,5,6), '_' )", nil, nil, "Returns array of 6 elements") 429 | eval("iterate(list(1,2,3,4,5,6), _ )", nil, nil, "Returns array of 6 elements; same result as previous") 430 | eval("#iterate(response.rooms,'void(i = i + \",\" + _.name)')", 0, nil, "Iterator using anonymous upvalue and empty result array") 431 | eval("#i", 254, nil, "Expected length of string may change if data altered") 432 | eval('#iterate(response.devices,"if(device.room==10,device.id)","device")', 7, nil, "Expected number of matching rooms may change if data altered") 433 | eval('#iterate(response.devices, if(device.room==10,device.id) , "device" )', 7, nil, "(LATE EVAL) Expected number of matching rooms may change if data altered") 434 | eval('map(list(6,5,4,3,2,1), _*16)', nil, nil, "Returns 6 elements with val = 16 x key (e.g. 3=48)") 435 | eval('map(list(6,5,4,3,2,1), "_*16")', nil, nil, "Result same as previous") 436 | eval('map(list("dog","cat","goldfish","ferret"))', nil, nil, "Returns table (key=val): dog=1,cat=2,goldfish=3,ferret=4") 437 | else 438 | nSkip = nSkip + 9 439 | end 440 | 441 | --[[ Not yet, maybe some day 442 | eval("Z=list()", nil, nil, "Set up for next test") 443 | eval("Z.abc=123", 123, nil, "Subref assignment") 444 | eval("Z.abc", 123, nil, "Subref assignment check") 445 | --]] 446 | end 447 | 448 | local function doMiscSyntaxTests() 449 | -- Variable assignment 450 | ctx.__lvars = ctx.__lvars or {} 451 | ctx.__lvars.lv = "lv" 452 | ctx.ctv = "ctv" 453 | ctx.k = nil ctx.__lvars.k = nil 454 | eval("lv", "lv") -- value sourced from __lvars (new style) 455 | eval("ctv", "ctv") -- value sourced from ctx (old style, deprecated) 456 | eval("i=25",25) 457 | eval("i", 25) 458 | if ctx.__lvars.i == nil or ctx.__lvars.i ~= 25 then fail("VARIABLE NOT FOUND IN __LVARS") end 459 | eval("k", nil, "Undefined var") 460 | 461 | -- Nesting 462 | eval("min(70,max(20,min(60,max(30,min(50,40)))))", 40) 463 | 464 | -- Quoted identifiers, subreferences, select() 465 | if ctx.response ~= nil then 466 | ctx.response['bad name!'] = ctx.response.loadtime 467 | eval("['response'].['bad name!']", ctx.response.loadtime, nil, "Quoted identifiers allow chars otherwise not permitted") 468 | eval("response.notthere", L.NULL) 469 | eval("response.notthere.reallynotthere", nil, "Can't dereference through null") 470 | ctx.__options = { nullderefnull=true } 471 | eval("response.notthere", L.NULL, nil, "with nullderefnull set") 472 | eval("response.notthere.reallynotthere", L.NULL, nil, "nullderefnull") 473 | ctx.__options = nil 474 | eval("select( response.rooms, 'id', '14' ).name", "Front Porch") 475 | else 476 | skip("['response'].['loadtime']", "JSON data not loaded") 477 | skip("response.notthere", "JSON data not loaded") 478 | skip("select( response.rooms, 'id', '14' ).name", "JSON data not loaded") 479 | skip("response.notthere.reallynotthere", nil, "Can't dereference through null") 480 | skip("select( response.rooms, 'id', '14' ).name", "Front Porch") 481 | end 482 | 483 | -- Syntax abuse 484 | eval("true=123", nil, "reserved word") 485 | eval("1,2", nil, "Invalid operator") 486 | eval("a[", nil, "Unexpected end of array subscript") 487 | eval("123+array]", nil, "Invalid operator") 488 | eval("+", nil, "Expected operand") 489 | 490 | -- Array subscripts 491 | ctx.__lvars = ctx.__lvars or {} 492 | ctx.__lvars.array = {11,22,33,44,55} 493 | eval("array[4]", 44) 494 | eval("array[19]", nil, "out of range") 495 | ctx.__options = { subscriptmissnull=true } 496 | eval("array[19]", L.NULL, nil, "with 'subscriptmissnull' set") 497 | ctx.__options = nil 498 | eval("i=list('A','B','C','D')", {'A','B','C','D'}, nil) 499 | eval("i[2]='X'", 'X', nil, "Array assignment") 500 | eval("i[2]", "X", nil) 501 | eval("#i", 4, nil) 502 | eval("i", {'A','X','C','D'}, nil) 503 | eval("k=4", 4, nil) 504 | eval("i[k]", "D", nil, "Array index vref") 505 | eval("i[k-1]", "C", nil, "Array index expression") 506 | eval("i [ k ]", "D", nil, "Array index vref excess whitespace") 507 | eval("i [ k-1 ]", "C", nil, "Array index expression excess whitespace") 508 | 509 | eval("true.val", nil, "Cannot subreference") 510 | eval("true[1]", nil, "not an array") 511 | eval("ff(1, )", nil, "Invalid subexpr") eval("ff( ,1)", nil, "Invalid subexpr") 512 | 513 | -- Custom functions 514 | ctx.__functions = { doublestring = function( argv ) return argv[1]..argv[1] end } 515 | eval("doublestring('galaxy')", "galaxygalaxy", nil, "Test custom function in __functions table (preferred)") 516 | ctx.dubstr = ctx.__functions.doublestring 517 | eval("dubstr('planet')", "planetplanet", nil, "Test custom function in context root (deprecated)") 518 | 519 | -- External resolver 520 | ctx.__functions = { __resolve = function( name, ctx ) if name == "MagicName" then return "Magic String" else return nil end end } 521 | eval("MagicName+' was found'", "Magic String was found", nil, "Test last-resort resolver (name found)") 522 | eval("PlainName", nil, "Undefined variable", "Test last-resort resolver (name not found)") 523 | 524 | eval("tonumber ( 123 )", 123, nil, "Excess whitespace in function ref") 525 | 526 | ctx.__functions = nil 527 | end 528 | 529 | local function doNullTests() 530 | eval("null", L.NULL) 531 | eval("nil", L.NULL) 532 | eval("null*4", nil, "Can't coerce null") 533 | eval("null/null", nil, "Can't coerce null") 534 | eval("tostring(null)", "") 535 | eval("null + 'abc'", "abc") 536 | eval("null & 123", false, nil, "null coerces to boolean false, and bool & number = bool") 537 | eval("null==0", false) 538 | eval("null==null", true) 539 | eval("null~=null", false) 540 | eval("null==1", false) 541 | eval("null==true", false) 542 | eval("null==false", true) 543 | eval("null==''", true) 544 | eval("len(null)", 0, nil, "The length of null is zero.") 545 | eval("null and false", L.NULL, nil, "Lua style") 546 | eval("null & false", false, nil, "C style (null coerced to boolean before op)") 547 | eval("null and true", L.NULL) 548 | eval("null & true", false) 549 | eval("true and null", L.NULL) 550 | eval("true & null", false) 551 | eval("false and null", false) 552 | eval("false & null", false) 553 | eval("null or true", true) 554 | eval("null or false", false) 555 | eval("true or null", true) 556 | eval("false or null", L.NULL) 557 | eval("false | null", false, nil, "C style") 558 | end 559 | 560 | local function doRegressionTests() 561 | local t = ctx -- save current context 562 | 563 | -- For this test, use special context. 564 | local s = '{"coord":{"lon":-84.56,"lat":33.39},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"stations","main":{"temp":281.29,"pressure":1026,"humidity":23,"temp_min":278.15,"temp_max":285.15},"visibility":16093,"wind":{"speed":5.1,"deg":150},"clouds":{"all":1},"dt":1517682900,"sys":{"type":1,"id":789,"message":0.0041,"country":"US","sunrise":1517661125,"sunset":1517699557},"id":0,"name":"Peachtree City","cod":200}' 565 | ctx = { response = json.decode(s) } 566 | eval("response.weather[1].description", "clear sky") 567 | eval("if( response.fuzzball==null, 'NO DATA', response.fuzzball.description )", "NO DATA", nil, "Late eval") 568 | eval("if( response['fuzzball']==null, 'NO DATA', response.fuzzball.description )", "NO DATA", nil, "Late eval") 569 | 570 | -- Special context here as well. 571 | ctx = json.decode('{"val":8,"ack":true,"ts":1517804967381,"q":0,"from":"system.adapter.mihome-vacuum.0","lc":"xyz","_id":"mihome-vacuum.0.info.state","type":"state","common":{"name":"Vacuum state","type":"number","read":true,"max":30,"states":{"1":"Unknown 1","2":"Sleep no Charge","3":"Sleep","5":"Cleaning","6":"Returning home","7":"Manuell mode","8":"Charging","10":"Paused","11":"Spot cleaning","12":"Error?!"}},"native":{}}') 572 | ctx = { response=ctx } 573 | eval("response.val", 8, nil, "Specific test for atom mis-identification (issue X) discovered by SiteSensor user") 574 | 575 | ctx = t -- restore prior context 576 | end 577 | 578 | -- Load JSON data into context, if we can. 579 | if json then 580 | local file = io.open( testData, "r" ) 581 | if file then 582 | local s = file:read("*all") 583 | file:close() 584 | ctx.response = json.decode(s) 585 | else 586 | print(RED.."JSON data could not be loaded!"..RESET) 587 | end 588 | end 589 | 590 | --[[ 591 | --]] 592 | print("luaxp.null is "..tostring(L.NULL)) 593 | doNumericParsingTests() 594 | doNullTests() 595 | doNumericOpsTests() 596 | doStringOpsTests() 597 | doBooleanOpsTests() 598 | doMathFuncTests() 599 | doStringFuncTests() 600 | doTimeTests() 601 | doMiscSyntaxTests() 602 | doMiscFuncTests() 603 | doRegressionTests() 604 | 605 | print("") 606 | print(string.format("Using module %s %s, ran %d tests, %d skipped, %d errors.", moduleName, tostring(L._VERSION), nTest, nSkip, nErr)) 607 | if ctx.response == nil then 608 | print(RED.."JSON data not loaded, some tests skipped"..RESET) 609 | end 610 | -------------------------------------------------------------------------------- /luaxp.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------ 2 | -- LuaXP is a simple expression evaluator for Lua, based on lexp.js, a 3 | -- lightweight (math) expression evaluator for JavaScript by the same 4 | -- author. 5 | -- 6 | -- Author: Copyright (c) 2016,2018 Patrick Rigney 7 | -- License: MIT License 8 | -- Github: https://github.com/toggledbits/luaxp 9 | ------------------------------------------------------------------------ 10 | 11 | local _M = {} 12 | 13 | _M._VERSION = "1.0.1" 14 | _M._VNUMBER = 10001 15 | _M._DEBUG = false -- Caller may set boolean true or function(msg) 16 | 17 | -- Binary operators and precedence (lower prec is higher precedence) 18 | _M.binops = { 19 | { op='.', prec=-1 } 20 | , { op='*', prec= 3 } 21 | , { op='/', prec= 3 } 22 | , { op='%', prec= 3 } 23 | , { op='+', prec= 4 } 24 | , { op='-', prec= 4 } 25 | , { op='<', prec= 6 } 26 | , { op='..', prec= 5 } 27 | , { op='<=', prec= 6 } 28 | , { op='>', prec= 6 } 29 | , { op='>=', prec= 6 } 30 | , { op='==', prec= 7 } 31 | , { op='<>', prec= 7 } 32 | , { op='!=', prec= 7 } 33 | , { op='~=', prec= 7 } 34 | , { op='&', prec= 8 } 35 | , { op='^', prec= 9 } 36 | , { op='|', prec=10 } 37 | , { op='&&', prec=11 } 38 | , { op='and', prec=11 } 39 | , { op='||', prec=12 } 40 | , { op='or', prec=12 } 41 | , { op='=', prec=14 } 42 | } 43 | 44 | local MAXPREC = 99 -- value doesn't matter as long as it's >= any used in binops 45 | 46 | local string = require("string") 47 | local math = require("math") 48 | local base = _G 49 | 50 | local CONST = 'const' 51 | local VREF = 'vref' 52 | local FREF = 'fref' 53 | local UNOP = 'unop' 54 | local BINOP = 'binop' 55 | local TNUL = 'null' 56 | 57 | local NULLATOM = { __type=TNUL } 58 | setmetatable( NULLATOM, { __tostring=function() return "null" end } ) 59 | 60 | local charmap = { t = "\t", r = "\r", n = "\n" } 61 | 62 | local reservedWords = { 63 | ['false']=false, ['true']=true 64 | , pi=math.pi, PI=math.pi 65 | , ['null']=NULLATOM, ['NULL']=NULLATOM, ['nil']=NULLATOM 66 | } 67 | 68 | local function dump(t, seen) 69 | if seen == nil then seen = {} end 70 | local typ = base.type(t) 71 | if typ == "table" and seen[t]==nil then 72 | seen[t] = 1 73 | local st = "{ " 74 | local first = true 75 | for n,v in pairs(t) do 76 | if not first then st = st .. ", " end 77 | st = st .. n .. "=" .. dump(v, seen) 78 | first = false 79 | end 80 | st = st .. " }" 81 | return st 82 | elseif typ == "string" then 83 | return string.format("%q", t) 84 | elseif typ == "boolean" or typ == "number" then 85 | return tostring(t) 86 | end 87 | return string.format("(%s)%s", typ, tostring(t)) 88 | end 89 | 90 | -- Debug output function. If _DEBUG is false or nil, no output. 91 | -- If function, uses that, otherwise print() 92 | local function D(s, ...) 93 | if not _M._DEBUG then return end 94 | local str = string.gsub(s, "%%(%d+)", function( n ) 95 | n = tonumber(n, 10) 96 | if n < 1 or n > #arg then return "nil" end 97 | local val = arg[n] 98 | if base.type(val) == "table" then 99 | return dump(val) 100 | elseif base.type(val) == "string" then 101 | return string.format("%q", val) 102 | end 103 | return tostring(val) 104 | end 105 | ) 106 | if base.type(_M._DEBUG) == "function" then _M._DEBUG(str) else print(str) end 107 | end 108 | 109 | -- Forward declarations 110 | local _comp, _run, runfetch, scan_token 111 | 112 | -- Utility functions 113 | 114 | local function deepcopy( t ) 115 | if base.type(t) ~= "table" then return t end 116 | local r = {} 117 | for k,v in pairs( t ) do 118 | if base.type(v) == "table" then 119 | r[k] = deepcopy(v) 120 | else 121 | r[k] = v 122 | end 123 | end 124 | return r 125 | end 126 | 127 | -- Value is atom if it matches our atom pattern, and specific atom type if passed 128 | local function isAtom( v, typ ) 129 | return base.type(v) == "table" and v.__type ~= nil and ( typ == nil or v.__type == typ ) 130 | end 131 | 132 | -- Special case null atom 133 | local function isNull( v ) 134 | return isAtom( v, TNUL ) 135 | end 136 | 137 | local function comperror(msg, loc) 138 | D("throwing comperror at %1: %2", loc, msg) 139 | return error( { __source='luaxp', ['type']='compile', location=loc, message=msg } ) 140 | end 141 | 142 | local function evalerror(msg, loc) 143 | D("throwing evalerror at %1: %2", loc, msg) 144 | return error( { __source='luaxp', ['type']='evaluation', location=loc, message=msg } ) 145 | end 146 | 147 | local function xp_pow( argv ) 148 | local b,x = unpack( argv or {} ) 149 | return math.exp(x * math.log(b)) 150 | end 151 | 152 | local function xp_select( argv ) 153 | local obj,keyname,keyval = unpack( argv or {} ) 154 | if base.type(obj) ~= "table" then evalerror("select() requires table/object arg 1") end 155 | keyname = tostring(keyname) 156 | keyval = tostring(keyval) 157 | for _,v in pairs(obj) do 158 | if tostring(v[keyname]) == keyval then 159 | return v 160 | end 161 | end 162 | return NULLATOM 163 | end 164 | 165 | local monthNameMap = {} 166 | local function mapLocaleMonth( m ) 167 | if m == nil then error("nil month name") end 168 | local ml = string.lower(tostring(m)) 169 | if ml:match("^%d+$") then 170 | -- All numeric. Simply return numeric form if valid range. 171 | local k = tonumber(ml) or 0 172 | if k >=1 and k <= 12 then return k end 173 | end 174 | if monthNameMap[ml] ~= nil then -- cached result? 175 | D("mapLocaleMonth(%1) cached result=%2", ml, monthNameMap[ml]) 176 | return monthNameMap[ml] 177 | end 178 | -- Since we can't get locale information directly in a platform-independent way, 179 | -- deduce it from live results... 180 | local d = os.date("*t") -- current time and date 181 | d.day = 1 -- pinned 182 | for k = 1,12 do 183 | d.month = k 184 | local tt = os.time(d) 185 | local s = os.date("#%b#%B#", tt):lower() 186 | if s:find("#"..ml.."#") then 187 | monthNameMap[ml] = k 188 | return k 189 | end 190 | end 191 | return evalerror("Cannot parse month name '" .. m .. "'") 192 | end 193 | 194 | local YMD=0 195 | local DMY=1 196 | local MDY=2 197 | local function guessMDDM() 198 | local d = os.date( "%x", os.time( { year=2001, month=8, day=22, hour=0 } ) ) 199 | local p = { d:match("(%d+)([/-])(%d+)[/-](%d+)") } 200 | if p[1] == "2001" then return YMD,p[2] 201 | elseif tonumber(p[1]) == 22 then return DMY,p[2] 202 | else return MDY,p[2] end 203 | end 204 | 205 | -- Somewhat simple time parsing. Handles the most common forms of ISO 8601, plus many less regular forms. 206 | -- If mm/dd vs dd/mm is ambiguous, it tries to discern using current locale's rule. 207 | local function xp_parse_time( t ) 208 | if base.type(t) == "number" then return t end -- if already numeric, assume it's already timestamp 209 | if t == nil or tostring(t):lower() == "now" then return os.time() end 210 | t = tostring(t) -- force string 211 | local now = os.time() 212 | local nd = os.date("*t", now) -- consistent 213 | local tt = { year=nd.year, month=nd.month, day=nd.day, hour=0, ['min']=0, sec=0 } 214 | local offset = 0 215 | -- Try to match a date. Start with two components. 216 | local order = nil 217 | local p = { t:match("^%s*(%d+)([/-])(%d+)(.*)") } -- entirely numeric w/sep 218 | if p[3] == nil then D("match 2") p = { t:match("^%s*(%d+)([/-])(%a+)(.*)") } order=DMY end -- number-word (4-Jul) 219 | if p[3] == nil then D("match 3") p = { t:match("^%s*(%a+)([/-])(%d+)(.*)") } order=MDY end -- word-number (Jul-4) 220 | if p[3] ~= nil then 221 | -- Look ahead for third component behind same separator 222 | D("Found p1=%1, p2=%2, sep=%3, rem=%4", p[1], p[2], p[3], p[4]) 223 | local sep = p[2] 224 | t = p[4] or "" 225 | D("Scanning for 3rd part from: '%1'", t) 226 | p[4],p[5] = t:match("^%" .. sep .. "(%d+)(.*)") 227 | if p[4] == nil then 228 | p[4] = tt.year 229 | else 230 | t = p[5] or "" -- advance token 231 | end 232 | -- We now have three components. Figure out their order. 233 | p[5]=t p[6]=p[6]or"" D("p=%1,%2,%3,%4,%5", unpack(p)) 234 | local first = tonumber(p[1]) or 0 235 | if order == nil and first > 31 then 236 | -- First is year (can't be month or day), assume y/m/d 237 | tt.year = first 238 | tt.month = mapLocaleMonth(p[3]) 239 | tt.day = p[4] 240 | elseif order == nil and first > 12 then 241 | -- First is day, assume d/m/y 242 | tt.day = first 243 | tt.month = mapLocaleMonth(p[3]) 244 | tt.year = p[4] 245 | else 246 | -- Guess using locale formatting 247 | if order == nil then 248 | D("Guessing MDY order") 249 | order = guessMDDM() 250 | end 251 | D("MDY order is %1", order) 252 | if order == 0 then 253 | tt.year = p[1] tt.month = mapLocaleMonth(p[3]) tt.day = p[4] 254 | elseif order == 1 then 255 | tt.day = p[1] tt.month = mapLocaleMonth(p[3]) tt.year = p[4] 256 | else 257 | tt.month = mapLocaleMonth(p[1]) tt.day = p[3] tt.year = p[4] 258 | end 259 | end 260 | tt.year = tonumber(tt.year) 261 | if tt.year < 100 then tt.year = tt.year + 2000 end 262 | D("Parsed date year=%1, month=%2, day=%3", tt.year, tt.month, tt.day) 263 | else 264 | -- YYYYMMDD? 265 | D("No match to delimited") 266 | p = { t:match("^%s*(%d%d%d%d)(%d%d)(%d%d)(.*)") } 267 | if p[3] ~= nil then 268 | tt.year = p[1] 269 | tt.month = p[2] 270 | tt.day = p[3] 271 | t = p[4] or "" 272 | else 273 | D("check %%c format") 274 | -- Fri Aug 4 16:18:22 2017 275 | p = { t:match("^%s*%a+%s+(%a+)%s+(%d+)(.*)") } -- with dow 276 | if p[2] == nil then p = { t:match("^%s*(%a+)%s+(%d+)(.*)") } end -- without dow 277 | if p[2] ~= nil then 278 | D("Matches %%c format, 1=%1,2=%2,3=%3", p[1], p[2], p[3]) 279 | tt.day = p[2] 280 | tt.month = mapLocaleMonth(p[1]) 281 | t = p[3] or "" 282 | -- Following time and year? 283 | p = { t:match("^%s*([%d:]+)%s+(%d%d%d%d)(.*)") } 284 | if p[1] ~= nil then 285 | tt.year = p[2] 286 | t = (p[1] or "") .. " " .. (p[3] or "") 287 | else 288 | -- Maybe just year? 289 | p = { t:match("^%s*(%d%d%d%d)(.*)") } 290 | if p[1] ~= nil then 291 | tt.year = p[1] 292 | t = p[2] or "" 293 | end 294 | end 295 | else 296 | D("No luck with any known date format.") 297 | end 298 | end 299 | D("Parsed date year=%1, month=%2, day=%3", tt.year, tt.month, tt.day) 300 | end 301 | -- Time? Note: does not support decimal fractions except on seconds component, which is ignored (ISO 8601 allows on any, but must be last component) 302 | D("Scanning for time from: '%1'", t) 303 | local hasTZ = false 304 | p = { t:match("^%s*T?(%d%d)(%d%d)(.*)") } -- ISO 8601 (Thhmm) without delimiters 305 | if p[1] == nil then p = { t:match("^%s*T?(%d+):(%d+)(.*)") } end -- with delimiters 306 | if p[1] ~= nil then 307 | -- Hour and minute 308 | tt.hour = p[1] 309 | tt['min'] = p[2] 310 | t = p[3] or "" 311 | -- Seconds? 312 | p = { t:match("^:?(%d+)(.*)") } 313 | if p[1] ~= nil then 314 | tt.sec = p[1] 315 | t = p[2] or "" 316 | end 317 | -- Swallow decimal on last component? 318 | p = { t:match("^(%.%d+)(.*)") } 319 | if p[1] ~= nil then 320 | t = p[2] or "" 321 | end 322 | -- AM or PM? 323 | p = { t:match("^%s*([AaPp])[Mm]?(.*)") } 324 | if p[1] ~= nil then 325 | D("AM/PM is %1", p[1]) 326 | if p[1]:lower() == "p" then tt.hour = tt.hour + 12 end 327 | t = p[2] or "" 328 | end 329 | D("Parsed time is %1:%2:%3", tt.hour, tt['min'], tt.sec) 330 | 331 | -- Timezone Zulu? 332 | p = { t:match("^([zZ])(.*)") } -- no whitespace, see comment below. 333 | if p[1] ~= nil then 334 | -- Zulu 335 | offset = 0 336 | hasTZ = true 337 | t = p[2] or "" 338 | end 339 | -- Handling for zones? UTC, GMT, minimally... what about others... EDT, JST, ...? 340 | -- Offset +/-HH[mm] (e.g. +02, -0500). Not that the pattern requires the TZ spec 341 | -- to follow the time without spaces between, to distinguish TZ from offsets (below). 342 | p = { t:match("^([+-]%d%d)(.*)") } 343 | if p[1] ~= nil then 344 | hasTZ = true 345 | offset = 60 * tonumber(p[1]) 346 | t = p[2]; 347 | p = { t:match("^:?(%d%d)(.*)") } 348 | if p[1] ~= nil then 349 | if offset < 0 then offset = offset - tonumber(p[1]) 350 | else offset = offset + tonumber(p[1]) 351 | end 352 | t = p[2] or "" 353 | end 354 | end 355 | end 356 | -- Is there an offset? Form is (+/-)DDD:HH:MM:SS. If parts are omitted, the offset 357 | -- is parsed from smallest to largest, so +05:00 is +5 minutes, -35 is minus 35 seconds. 358 | local delta = 0 359 | D("Checking for offset from '%1'", t) 360 | p = { t:match("%s*([+-])(%d+)(.*)") } 361 | if p[2] ~= nil then 362 | D("Parsing offset from %1, first part is %2", t, p[2]) 363 | local sign = p[1] 364 | delta = tonumber(p[2]) 365 | if delta == nil then evalerror("Invalid delta spec: " .. t) end 366 | t = p[3] or "" 367 | for k = 1,3 do 368 | D("Parsing offset from %1", t) 369 | p = { t:match("%:(%d+)(.*)") } 370 | if p[1] == nil then break end 371 | if k == 3 then delta = delta * 24 else delta = delta * 60 end 372 | delta = delta + tonumber(p[1]) 373 | t = p[2] or "" 374 | end 375 | if sign == "-" then delta = -delta end 376 | D("Final delta is %1", delta) 377 | end 378 | -- There should not be anything left at this point 379 | if t:match("([^%s])") then 380 | return evalerror("Unparseable data: " .. t) 381 | end 382 | local tm = os.time( tt ) 383 | if hasTZ then 384 | -- If there's a timezone spec, apply it. Otherwise we assume time was in current (system) TZ 385 | -- and leave it unmodified. 386 | local loctime = os.date( "*t", tm ) -- get new local time's DST flag 387 | local epoch = { year=1970, month=1, day=1, hour=0 } 388 | epoch.isdst = loctime.isdst -- 19084 fix, maybe need Reactor approach? 389 | local locale_offset = os.time( epoch ) 390 | tm = tm - locale_offset -- back to UTC, because conversion assumes current TZ, so undo that. 391 | tm = tm - ( offset * 60 ) -- apply specified offset 392 | end 393 | tm = tm + delta 394 | return tm -- returns time in UTC 395 | end 396 | 397 | -- Date add. First arg is timestamp, then secs, mins, hours, days, months, years 398 | local function xp_date_add( a ) 399 | local tm = xp_parse_time( a[1] ) 400 | if a[2] ~= nil then tm = tm + (tonumber(a[2]) or evalerror("Invalid seconds (argument 2) to dateadd()")) end 401 | if a[3] ~= nil then tm = tm + 60 * (tonumber(a[3]) or evalerror("Invalid minutes (argument 3) to dateadd()")) end 402 | if a[4] ~= nil then tm = tm + 3600 * (tonumber(a[4]) or evalerror("Invalid hours (argument 4) to dateadd()")) end 403 | if a[5] ~= nil then tm = tm + 86400 * (tonumber(a[5]) or evalerror("Invalid days (argument 5) to dateadd()")) end 404 | if a[6] ~= nil or a[7] ~= nil then 405 | D("Applying delta months and years to %1", tm) 406 | local d = os.date("*t", tm) 407 | d.month = d.month + ( tonumber( a[6] ) or 0 ) 408 | d.year = d.year + ( tonumber( a[7] ) or 0 ) 409 | D("Normalizing month,year=%1,%2", d.month, d.year) 410 | while d.month < 1 do 411 | d.month = d.month + 12 412 | d.year = d.year - 1 413 | end 414 | while d.month > 12 do 415 | d.month = d.month - 12 416 | d.year = d.year + 1 417 | end 418 | tm = os.time(d) 419 | end 420 | return tm 421 | end 422 | 423 | -- Delta between two times. Returns value in seconds. 424 | local function xp_date_diff( d1, d2 ) 425 | return xp_parse_time( d1 ) - xp_parse_time( d2 or os.time() ) 426 | end 427 | 428 | -- Create a timestamp for date/time in the current timezone or UTC by parts 429 | local function xp_mktime( yy, mm, dd, hours, mins, secs ) 430 | local pt = os.date("*t") 431 | pt.year = tonumber(yy) or pt.year 432 | pt.month = tonumber(mm) or pt.month 433 | pt.day = tonumber(dd) or pt.day 434 | pt.hour = tonumber(hours) or pt.hour 435 | pt.min = tonumber(mins) or pt.min 436 | pt.sec = tonumber(secs) or pt.sec 437 | pt.isdst = nil 438 | pt.yday = nil 439 | pt.wday = nil 440 | return os.time(pt) 441 | end 442 | 443 | local function xp_rtrim(s) 444 | if base.type(s) ~= "string" then evalerror("String required") end 445 | return s:gsub("%s+$", "") 446 | end 447 | 448 | local function xp_ltrim(s) 449 | if base.type(s) ~= "string" then evalerror("String required") end 450 | return s:gsub("^%s+", "") 451 | end 452 | 453 | local function xp_trim( s ) 454 | if base.type(s) ~= "string" then evalerror("String required") end 455 | return xp_ltrim( xp_rtrim( s ) ) 456 | end 457 | 458 | local function xp_keys( argv ) 459 | local arr = unpack( argv or {} ) 460 | if base.type( arr ) ~= "table" then evalerror("Array/table required") end 461 | local r = {} 462 | for k in pairs( arr ) do 463 | if k ~= "__context" then 464 | table.insert( r, k ) 465 | end 466 | end 467 | return r 468 | end 469 | 470 | local function xp_tlen( t ) 471 | local n = 0 472 | for _ in pairs(t) do n = n + 1 end 473 | return n 474 | end 475 | 476 | local function xp_split( argv ) 477 | local str = tostring( argv[1] or "" ) 478 | local sep = argv[2] or "," 479 | local arr = {} 480 | if #str == 0 then return arr, 0 end 481 | local rest = string.gsub( str or "", "([^" .. sep .. "]*)" .. sep, function( m ) table.insert( arr, m ) return "" end ) 482 | table.insert( arr, rest ) 483 | return arr, #arr 484 | end 485 | 486 | local function xp_join( argv ) 487 | local a = argv[1] or {} 488 | if type(a) ~= "table" then evalerror("Argument 1 to join() is not an array") end 489 | local d = argv[2] or "," 490 | return table.concat( a, d ) 491 | end 492 | 493 | local function xp_min( argv ) 494 | local res = NULLATOM 495 | for _,v in ipairs( argv ) do 496 | local bv = v 497 | if type(v) == "table" then 498 | bv = xp_min( v ) 499 | end 500 | if type(bv)=="number" and ( res == NULLATOM or bv < res ) then 501 | res = bv 502 | end 503 | end 504 | return res 505 | end 506 | 507 | local function xp_max( argv ) 508 | local res = NULLATOM 509 | for _,v in ipairs( argv ) do 510 | local bv = v 511 | if type(v) == "table" then 512 | bv = xp_max( v ) 513 | end 514 | if type(bv)=="number" and ( res == NULLATOM or bv > res ) then 515 | res = bv 516 | end 517 | end 518 | return res 519 | end 520 | 521 | local msgNNA1 = "Non-numeric argument 1" 522 | 523 | -- ??? All these tostrings() need to be coerce() 524 | local nativeFuncs = { 525 | ['abs'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return (n<0) and -n or n end } 526 | , ['sgn'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return (n<0) and -1 or ((n==0) and 0 or 1) end } 527 | , ['floor'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.floor(n) end } 528 | , ['ceil'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.ceil(n) end } 529 | , ['round'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) local p = tonumber( argv[2] ) or 0 return math.floor( n * (10^p) + 0.5 ) / (10^p) end } 530 | , ['cos'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.cos(n) end } 531 | , ['sin'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.sin(n) end } 532 | , ['tan'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.tan(n) end } 533 | , ['asin'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.asin(n) end } 534 | , ['acos'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.acos(n) end } 535 | , ['atan'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.atan(n) end } 536 | , ['rad'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return n * math.pi / 180 end } 537 | , ['deg'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return n * 180 / math.pi end } 538 | , ['log'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.log(n) end } 539 | , ['exp'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.exp(n) end } 540 | , ['pow'] = { nargs = 2, impl = xp_pow } 541 | , ['sqrt'] = { nargs = 1, impl = function( argv ) local n = tonumber( argv[1] ) or evalerror(msgNNA1) return math.sqrt(n) end } 542 | , ['min'] = { nargs = 1, impl = xp_min } 543 | , ['max'] = { nargs = 1, impl = xp_max } 544 | , ['randomseed'] = { nargs = 0, impl = function( argv ) local s = argv[1] or os.time() math.randomseed(s) return s end } 545 | , ['random'] = { nargs = 0, impl = function( argv ) return math.random( unpack(argv) ) end } 546 | , ['len'] = { nargs = 1, impl = function( argv ) if isNull(argv[1]) then return 0 elseif type(argv[1]) == "table" then return xp_tlen(argv[1]) else return string.len(tostring(argv[1])) end end } 547 | , ['sub'] = { nargs = 2, impl = function( argv ) local st = tostring(argv[1]) local p = argv[2] local l = (argv[3] or -1) return string.sub(st, p, l) end } 548 | , ['find'] = { nargs = 2, impl = function( argv ) local st = tostring(argv[1]) local p = tostring(argv[2]) local i = argv[3] or 1 return (string.find(st, p, i) or 0) end } 549 | , ['upper'] = { nargs = 1, impl = function( argv ) return string.upper(tostring(argv[1])) end } 550 | , ['lower'] = { nargs = 1, impl = function( argv ) return string.lower(tostring(argv[1])) end } 551 | , ['trim'] = { nargs = 1, impl = function( argv ) return xp_trim(tostring(argv[1])) end } 552 | , ['ltrim'] = { nargs = 1, impl = function( argv ) return xp_ltrim(tostring(argv[1])) end } 553 | , ['rtrim'] = { nargs = 1, impl = function( argv ) return xp_rtrim(tostring(argv[1])) end } 554 | , ['tostring'] = { nargs = 1, impl = function( argv ) if isNull(argv[1]) then return "" else return tostring(argv[1]) end end } 555 | , ['tonumber'] = { nargs = 1, impl = function( argv ) if base.type(argv[1]) == "boolean" then if argv[1] then return 1 else return 0 end end return tonumber(argv[1], argv[2] or 10) or evalerror('Argument could not be converted to number') end } 556 | , ['format'] = { nargs = 1, impl = function( argv ) return string.format( unpack(argv) ) end } 557 | , ['split'] = { nargs = 1, impl = xp_split } 558 | , ['join'] = { nargs = 1, impl = xp_join } 559 | , ['time'] = { nargs = 0, impl = function( argv ) return xp_parse_time( argv[1] ) end } 560 | , ['timepart'] = { nargs = 0, impl = function( argv ) return os.date( argv[2] and "!*t" or "*t", argv[1] ) end } 561 | , ['date'] = { nargs = 0, impl = function( argv ) return xp_mktime( unpack(argv) ) end } 562 | , ['strftime'] = { nargs = 1, impl = function( argv ) return os.date(unpack(argv)) end } 563 | , ['dateadd'] = { nargs = 2, impl = function( argv ) return xp_date_add( argv ) end } 564 | , ['datediff'] = { nargs = 1, impl = function( argv ) return xp_date_diff( argv[1], argv[2] or os.time() ) end } 565 | , ['choose'] = { nargs = 2, impl = function( argv ) local ix = argv[1] if ix < 1 or ix > (#argv-2) then return argv[2] else return argv[ix+2] end end } 566 | , ['select'] = { nargs = 3, impl = xp_select } 567 | , ['keys'] = { nargs = 1, impl = xp_keys } 568 | , ['iterate'] = { nargs = 2, impl = true } 569 | , ['map'] = { nargs = 2, impl = true } 570 | , ['if'] = { nargs = 2, impl = true } 571 | , ['void'] = { nargs = 0, impl = function( argv ) return NULLATOM end } 572 | , ['list'] = { nargs = 0, impl = function( argv ) local b = deepcopy( argv ) b.__context=nil return b end } 573 | , ['first'] = { nargs = 1, impl = function( argv ) local arr = argv[1] if base.type(arr) ~= "table" or #arr == 0 then return NULLATOM else return arr[1] end end } 574 | , ['last'] = { nargs = 1, impl = function( argv ) local arr = argv[1] if base.type(arr) ~= "table" or #arr == 0 then return NULLATOM else return arr[#arr] end end } 575 | } 576 | 577 | -- Try to load bit module; fake it if we don't find it or not right. 578 | local _, bit = pcall( require, "bit" ) 579 | if not ( type(bit) == "table" and bit.band and bit.bor and bit.bnot and bit.bxor ) then 580 | bit = nil 581 | end 582 | if not bit then 583 | -- Adapted from "BitUtils", Lua-users wiki at http://lua-users.org/wiki/BitUtils; thank you kind stranger(s)... 584 | bit = {} 585 | bit['nand'] = function(x,y,z) 586 | z=z or 2^16 587 | if z<2 then 588 | return 1-x*y 589 | else 590 | return bit.nand((x-x%z)/z,(y-y%z)/z,math.sqrt(z))*z+bit.nand(x%z,y%z,math.sqrt(z)) 591 | end 592 | end 593 | bit["bnot"]=function(y,z) return bit.nand(bit.nand(0,0,z),y,z) end 594 | bit["band"]=function(x,y,z) return bit.nand(bit["bnot"](0,z),bit.nand(x,y,z),z) end 595 | bit["bor"]=function(x,y,z) return bit.nand(bit["bnot"](x,z),bit["bnot"](y,z),z) end 596 | bit["bxor"]=function(x,y,z) return bit["band"](bit.nand(x,y,z),bit["bor"](x,y,z),z) end 597 | end 598 | 599 | -- Let's get to work 600 | 601 | -- Skips white space, returns index of non-space character or nil 602 | local function skip_white( expr, index ) 603 | D("skip_white from %1 in %2", index, expr) 604 | local _,e = string.find( expr, "^%s+", index ) 605 | if e then index = e + 1 end -- whitespace(s) found, return pos after 606 | return index 607 | end 608 | 609 | -- Scan a numeric token. Supports fractional and exponent specs in 610 | -- decimal numbers, and binary, octal, and hexadecimal integers. 611 | local function scan_numeric( expr, index ) 612 | D("scan_numeric from %1 in %2", index, expr) 613 | local len = string.len(expr) 614 | local ch, i 615 | local val = 0 616 | local radix = 0 617 | -- Try to guess the radix first 618 | ch = string.sub(expr, index, index) 619 | if ch == '0' and index < len then 620 | -- Look to next character 621 | index = index + 1 622 | ch = string.sub(expr, index, index) 623 | if ch == 'b' or ch == 'B' then 624 | radix = 2 625 | index = index + 1 626 | elseif ch == 'x' or ch == 'X' then 627 | radix = 16 628 | index = index + 1 629 | elseif ch == '.' then 630 | radix = 10 -- going to be a decimal number 631 | else 632 | radix = 8 633 | end 634 | end 635 | if radix <= 0 then radix = 10 end 636 | -- Now parse the whole part of the number 637 | while (index <= len) do 638 | ch = string.sub(expr, index, index) 639 | if ch == '.' then break end 640 | i = string.find("0123456789ABCDEF", string.upper(ch), 1, true) 641 | if i == nil or ( radix==10 and i==15 ) then break end 642 | if i > radix then comperror("Invalid digit for radix "..radix, index) end 643 | val = radix * val + (i-1) 644 | index = index + 1 645 | end 646 | -- Parse fractional part, if any 647 | if ch == '.' and radix==10 then 648 | local ndec = 0 649 | index = index + 1 -- get past decimal point 650 | while (index <= len) do 651 | ch = string.sub(expr, index, index) 652 | i = string.byte(ch) - 48 653 | if i<0 or i>9 then break end 654 | ndec = ndec + 1 655 | val = val + ( i * 10 ^ -ndec ) 656 | index = index + 1 657 | end 658 | end 659 | -- Parse exponent, if any 660 | if (ch == 'e' or ch == 'E') and radix == 10 then 661 | local npow = 0 662 | local neg = nil 663 | index = index + 1 -- get base exponent marker 664 | local st = index 665 | while (index <= len) do 666 | ch = string.sub(expr, index, index) 667 | if neg == nil and ch == "-" then neg = true 668 | elseif neg == nil and ch == "+" then neg = false 669 | else 670 | i = string.byte(ch) - 48 671 | if i<0 or i>9 then break end 672 | npow = npow * 10 + i 673 | if neg == nil then neg = false end 674 | end 675 | index = index + 1 676 | end 677 | 678 | if index == st then comperror("Missing exponent", index) end 679 | if neg then npow = -npow end 680 | val = val * ( 10 ^ npow ) 681 | end 682 | -- Return result 683 | D("scan_numeric returning index=%1, val=%2", index, val) 684 | return index, { __type=CONST, value=val } 685 | end 686 | 687 | -- Parse a string. Trivial at the moment and needs escaping of some kind 688 | local function scan_string( expr, index ) 689 | D("scan_string from %1 in %2", index, expr) 690 | local len = string.len(expr) 691 | local st = "" 692 | local i 693 | local qchar = string.sub(expr, index, index) 694 | index = index + 1 695 | while index <= len do 696 | i = string.sub(expr, index, index) 697 | if i == '\\' and index < len then 698 | index = index + 1 699 | i = string.sub(expr, index, index) 700 | if charmap[i] then i = charmap[i] end 701 | elseif i == qchar then 702 | -- PHR??? Should we do the double char style of quoting? don''t won''t ?? 703 | index = index + 1 704 | return index, { __type=CONST, value=st } 705 | end 706 | st = st .. i 707 | index = index + 1 708 | end 709 | return comperror("Unterminated string", index) 710 | end 711 | 712 | -- Parse a function reference. It is treated as a degenerate case of 713 | -- variable reference, i.e. an alphanumeric string followed immediately 714 | -- by an opening parenthesis. 715 | local function scan_fref( expr, index, name ) 716 | D("scan_fref from %1 in %2", index, expr) 717 | local len = string.len(expr) 718 | local args = {} 719 | local parenLevel = 1 720 | local ch 721 | local subexp = "" 722 | index = skip_white( expr, index ) + 1 723 | while ( true ) do 724 | if index > len then return comperror("Unexpected end of argument list", index) end -- unexpected end of argument list 725 | 726 | ch = string.sub(expr, index, index) 727 | if ch == ')' then 728 | D("scan_fref: Found a closing paren while at level %1", parenLevel) 729 | parenLevel = parenLevel - 1 730 | if parenLevel == 0 then 731 | subexp = xp_trim( subexp ) 732 | D("scan_fref: handling end of argument list with subexp=%1", subexp) 733 | if string.len(subexp) > 0 then -- PHR??? Need to test out all whitespace strings from the likes of "func( )" 734 | table.insert(args, _comp( subexp ) ) -- compile the subexp and put it on the list 735 | elseif #args > 0 then 736 | comperror("Invalid subexpression", index) 737 | end 738 | index = index + 1 739 | D("scan_fref returning, function is %1 with %2 args", name, #args, dump(args)) 740 | return index, { __type=FREF, args=args, name=name, pos=index } 741 | else 742 | -- It's part of our argument, so just add it to the subexpress string 743 | subexp = subexp .. ch 744 | index = index + 1 745 | end 746 | elseif ch == "'" or ch == '"' then 747 | -- Start of string? 748 | local qq = ch 749 | index, ch = scan_string( expr, index ) 750 | subexp = subexp .. qq .. ch.value .. qq 751 | elseif ch == ',' and parenLevel == 1 then -- completed subexpression 752 | subexp = xp_trim( subexp ) 753 | D("scan_fref: handling argument=%1", subexp) 754 | if string.len(subexp) > 0 then 755 | local r = _comp(subexp) 756 | if r == nil then return comperror("Subexpression failed to compile", index) end 757 | table.insert( args, r ) 758 | D("scan_fref: inserted argument %1 as %2", subexp, r) 759 | else 760 | comperror("Invalid subexpression", index) 761 | end 762 | index = skip_white( expr, index+1 ) 763 | subexp = "" 764 | D("scan_fref: continuing argument scan in %1 from %2", expr, index) 765 | else 766 | subexp = subexp .. ch 767 | if ch == '(' then parenLevel = parenLevel + 1 end 768 | index = index + 1 769 | end 770 | end 771 | end 772 | 773 | -- Parse an array reference 774 | local function scan_aref( expr, index, name ) 775 | D("scan_aref from %1 in %2", index, expr) 776 | local len = string.len(expr) 777 | local ch 778 | local subexp = "" 779 | local depth = 0 780 | index = skip_white( expr, index ) + 1 781 | while ( true ) do 782 | if index > len then return comperror("Unexpected end of array subscript expression", index) end 783 | ch = string.sub(expr, index, index) 784 | if ch == ']' then 785 | if depth == 0 then 786 | D("scan_aref: Found a closing bracket, subexp=%1", subexp) 787 | local args = _comp(subexp) 788 | D("scan_aref returning, array is %1", name) 789 | return index+1, { __type=VREF, name=name, index=args, pos=index } 790 | end 791 | depth = depth - 1 792 | elseif ch == "[" then 793 | depth = depth + 1 794 | end 795 | subexp = subexp .. ch 796 | index = index + 1 797 | end 798 | end 799 | 800 | -- Scan a variable reference; could turn into a function reference 801 | local function scan_vref( expr, index ) 802 | D("scan_vref from %1 in %2", index, expr) 803 | local len = string.len(expr); 804 | local ch, k 805 | local name = "" 806 | while index <= len do 807 | ch = string.sub(expr, index, index) 808 | if string.find( expr, "^%s*%(", index ) then 809 | if name == "" then comperror("Invalid operator", index) end 810 | return scan_fref(expr, index, name) 811 | elseif string.find( expr, "^%s*%[", index ) then 812 | -- Possible that name is blank. We allow/endorse, for ['identifier'] form of vref (see runtime) 813 | return scan_aref(expr, index, name) 814 | end 815 | k = string.find("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_", string.upper(ch), 1, true) 816 | if k == nil then 817 | break 818 | elseif name == "" and k <= 10 then 819 | return comperror("Invalid identifier", index) 820 | end 821 | 822 | name = name .. ch 823 | index = index + 1 824 | end 825 | 826 | return index, { __type=VREF, name=name, pos=index } 827 | end 828 | 829 | -- Scan nested expression (called when ( seen while scanning for token) 830 | local function scan_expr( expr, index ) 831 | D("scan_expr from %1 in %2", index, expr) 832 | local len = string.len(expr) 833 | local st = "" 834 | local parenLevel = 0 835 | index = index + 1 836 | while index <= len do 837 | local ch = string.sub(expr,index,index) 838 | if ch == ')' then 839 | if parenLevel == 0 then 840 | D("scan_expr parsing subexpression=%1", st) 841 | local r = _comp( st ) 842 | if r == nil then return comperror("Subexpression failed to parse", index) end 843 | return index+1, r -- pass as single-element sub-expression 844 | end 845 | parenLevel = parenLevel - 1 846 | elseif ch == '(' then 847 | parenLevel = parenLevel + 1 848 | end 849 | -- Add character to subexpression string (note drop-throughs from above conditionals) 850 | st = st .. ch 851 | index = index + 1 852 | end 853 | return index, nil -- Unexpected end of expression/unmatched paren group 854 | end 855 | 856 | local function scan_unop( expr, index ) 857 | D("scan_unop from %1 in %2", index, expr) 858 | local len = string.len(expr) 859 | if index > len then return index, nil end 860 | local ch = string.sub(expr, index, index) 861 | if ch == '-' or ch == '+' or ch == '!' or ch == '#' then 862 | -- We have a UNOP 863 | index = index + 1 864 | local k, r = scan_token( expr, index ) 865 | if r == nil then return k, r end 866 | return k, { __type=UNOP, op=ch, pos=index, operand=r } 867 | end 868 | return index, nil -- Not a UNOP 869 | end 870 | 871 | local function scan_binop( expr, index ) 872 | D("scan_binop from %1 in %2", index, expr) 873 | local len = string.len(expr) 874 | index = skip_white(expr, index) 875 | if index > len then return index, nil end 876 | 877 | local op = "" 878 | local k = 0 879 | local prec 880 | while index <= len do 881 | local ch = string.sub(expr,index,index) 882 | local st = op .. ch 883 | local matched = false 884 | k = k + 1 885 | for _,f in ipairs(_M.binops) do 886 | if string.sub(f.op,1,k) == st then 887 | -- matches something 888 | matched = true 889 | prec = f.prec 890 | break; 891 | end 892 | end 893 | if not matched then 894 | -- Didn't match anything. If we matched nothing on the first character, that's an error. 895 | -- Otherwise, op now contains the name of the longest-matching binop in the catalog. 896 | if k == 1 then return comperror("Invalid operator", index) end 897 | break 898 | end 899 | 900 | -- Keep going to find longest match 901 | op = st 902 | index = index + 1 903 | end 904 | 905 | D("scan_binop succeeds with op=%1", op) 906 | return index, { __type=BINOP, op=op, prec=prec, pos=index } 907 | end 908 | 909 | -- Scan our next token (forward-declared) 910 | scan_token = function( expr, index ) 911 | D("scan_token from %1 in %2", index, expr) 912 | local len = string.len(expr) 913 | index = skip_white(expr, index) 914 | if index > len then return index, nil end 915 | 916 | local ch = string.sub(expr,index,index) 917 | D("scan_token guessing from %1 at %2", ch, index) 918 | if ch == '"' or ch=="'" then 919 | -- String literal 920 | return scan_string( expr, index ) 921 | elseif ch == '(' then 922 | -- Nested expression 923 | return scan_expr( expr, index ) 924 | elseif string.find("0123456789", ch, 1, true) ~= nil then 925 | -- Numeric token 926 | return scan_numeric( expr, index ) 927 | elseif ch == "." then 928 | -- Look ahead, could be number without leading 0 or subref 929 | if index < len and string.find("0123456789", string.sub(expr,index+1,index+1), 1, true) ~= nil then 930 | return scan_numeric( expr, index ) 931 | end 932 | end 933 | 934 | -- Check for unary operator 935 | local k, r 936 | k, r = scan_unop( expr, index ) 937 | if r ~= nil then return k, r end 938 | 939 | -- Variable or function reference? 940 | k, r = scan_vref( expr, index ) 941 | if r ~= nil then return k, r end 942 | 943 | --We've got no idea what we're looking at... 944 | return comperror("Invalid token",index) 945 | end 946 | 947 | local function parse_rpn( lexpr, expr, index, lprec ) 948 | D("parse_rpn: parsing %1 from %2 prec %3 lhs %4", expr, index, lprec, lexpr) 949 | local binop, rexpr, lop, ilast 950 | 951 | ilast = index 952 | index,lop = scan_binop( expr, index ) 953 | D("parse_rpn: outside lookahead is %1" ,lop) 954 | while (lop ~= nil and lop.prec <= lprec) do 955 | -- We're keeping this one 956 | binop = lop 957 | D("parse_rpn: mid at %1 handling ", index, binop) 958 | -- Fetch right side of expression 959 | index,rexpr = scan_token( expr, index ) 960 | D("parse_rpn: mid rexpr is %1", rexpr) 961 | if rexpr == nil then return comperror("Expected operand", ilast) end 962 | -- Peek at next operator 963 | ilast = index -- remember where we were 964 | index,lop = scan_binop( expr, index ) 965 | D("parse_rpn: mid lookahead is %1", lop) 966 | while (lop ~= nil and lop.prec < binop.prec) do 967 | index, rexpr = parse_rpn( rexpr, expr, ilast, lop.prec ) 968 | D("parse_rpn: inside rexpr is %1", rexpr) 969 | ilast = index 970 | index, lop = scan_binop( expr, index ) 971 | D("parse_rpn: inside lookahead is %1", lop) 972 | end 973 | binop.lexpr = lexpr 974 | binop.rexpr = rexpr 975 | lexpr = binop 976 | end 977 | D("parse_rpn: returning index %1 lhs %2", ilast, lexpr) 978 | return ilast, lexpr 979 | end 980 | 981 | -- Completion of forward declaration 982 | _comp = function( expr ) 983 | local index = 1 984 | local lhs 985 | 986 | expr = expr or "" 987 | expr = tostring(expr) 988 | D("_comp: parse %1", expr) 989 | 990 | index,lhs = scan_token( expr, index ) 991 | index,lhs = parse_rpn( lhs, expr, index, MAXPREC ) 992 | return lhs 993 | end 994 | 995 | -- Better version, checks one or two operands (AND logic result) 996 | local function check_operand( v1, allow1, v2, allow2 ) 997 | local vt = base.type(v1) 998 | local res = true 999 | if v2 ~= nil then 1000 | res = check_operand( v2, allow2 or allow1 ) 1001 | end 1002 | if res then 1003 | if base.type(allow1) == "string" then 1004 | res = (vt == allow1) 1005 | elseif base.type(allow1) ~= "table" then 1006 | error("invalid allow1") -- bug, only string and array allowed 1007 | else 1008 | res = false 1009 | for _,t in ipairs(allow1) do 1010 | if vt == t then 1011 | res = true 1012 | break 1013 | end 1014 | end 1015 | end 1016 | end 1017 | return res 1018 | end 1019 | 1020 | local function coerce(val, typ) 1021 | local vt = base.type(val) 1022 | D("coerce: attempt (%1)%2 to %3", vt, val, typ) 1023 | if vt == typ then return val end -- already there? 1024 | if typ == "boolean" then 1025 | -- Coerce to boolean 1026 | if vt == "number" then return val ~= 0 1027 | elseif vt == "string" then 1028 | if string.lower(val) == "true" or val == "yes" or val == "1" then return true 1029 | elseif string.lower(val) == "false" or val == "no" or val == "0" then return false 1030 | else return #val ~= 0 -- empty string is false, all else is true 1031 | end 1032 | elseif isNull(val) then return false -- null coerces to boolean false 1033 | end 1034 | elseif typ == "string" then 1035 | if vt == "number" then return tostring(val) 1036 | elseif vt == "boolean" then return val and "true" or "false" 1037 | elseif isNull(val) then return "" -- null coerces to empty string within expressions 1038 | end 1039 | elseif typ == "number" then 1040 | if vt == "boolean" then return val and 1 or 0 1041 | elseif vt == "string" then 1042 | local n = tonumber(val,10) -- TODO ??? needs more complete parser (hex/octal/bin) 1043 | if n ~= nil then return n else evalerror("Coersion from string to number failed ("..val..")") end 1044 | end 1045 | -- null coerces to NaN? We don't have NaN. Yet... 1046 | end 1047 | if isNull(val) then evalerror("Can't coerce null to " .. typ) end 1048 | evalerror("Can't coerce " .. vt .. " to " .. typ) 1049 | end 1050 | 1051 | local function isNumeric(val) 1052 | if isNull(val) then return false end 1053 | local s = tonumber(val, 10) 1054 | if s == nil then return false 1055 | else return true, s 1056 | end 1057 | end 1058 | 1059 | local function getOption( ctx, name ) 1060 | return ((ctx or {}).__options or {})[name] and true or false 1061 | end 1062 | 1063 | local function pop( stack ) 1064 | if #stack == 0 then return nil end 1065 | return table.remove( stack ) 1066 | end 1067 | 1068 | -- Pop an item off the stack. If it's a variable reference, resolve it now. 1069 | local function fetch( stack, ctx ) 1070 | local v 1071 | local e = pop( stack ) 1072 | if e == nil then evalerror("Missing expected operand") end 1073 | D("fetch() popped %1", e) 1074 | if isAtom( e, VREF ) then 1075 | D("fetch: evaluating VREF %1 to its value", e.name) 1076 | -- A bit of a kludge. If name is empty but index is defined, we have a quoted reference 1077 | -- such as ['response'], which allows access to identifiers with special characters. 1078 | if ( e.name or "" ) == "" and e.index ~= nil then 1079 | e.name = runfetch(e.index, ctx, stack) 1080 | e.index = nil 1081 | end 1082 | if reservedWords[e.name:lower()] ~= nil then 1083 | D("fetch: found reserved word %1 for VREF", e.name) 1084 | v = reservedWords[e.name:lower()] 1085 | elseif (ctx.__lvars or {})[e.name] ~= nil then 1086 | v = ctx.__lvars[e.name] 1087 | else 1088 | v = ctx[e.name] 1089 | end 1090 | -- If no value so far, check if external resolver is available; use if available. 1091 | if v == nil and (ctx.__functions or {}).__resolve ~= nil then 1092 | D("fetch: calling external resolver for %1", e.name) 1093 | v = ctx.__functions.__resolve( e.name, ctx ) 1094 | end 1095 | if v == nil then evalerror("Undefined variable: " .. e.name, e.pos) end 1096 | -- Apply array index if present 1097 | if e.index ~= nil then 1098 | if base.type(v) ~= "table" then evalerror(e.name .. " is not an array", e.pos) end 1099 | local ix = runfetch(e.index, ctx, stack) 1100 | D("fetch: applying subscript: %1[%2]", e.name, ix) 1101 | if ix ~= nil then 1102 | v = v[ix] 1103 | if v == nil then 1104 | if type(ix) == "number" then 1105 | if getOption( ctx, "subscriptmissnull" ) then 1106 | v = NULLATOM 1107 | else 1108 | evalerror("Subscript " .. ix .. " out of range for " .. e.name, e.pos) 1109 | end 1110 | else 1111 | v = NULLATOM 1112 | end 1113 | end 1114 | else 1115 | evalerror("Subscript evaluation failed", e.pos) 1116 | end 1117 | end 1118 | return v 1119 | end 1120 | return e 1121 | end 1122 | 1123 | runfetch = function( atom, ctx, stack ) 1124 | _run( atom, ctx, stack ) 1125 | return fetch( stack, ctx ) 1126 | end 1127 | 1128 | _run = function( atom, ctx, stack ) 1129 | if not isAtom( atom ) then D("Invalid atom: %1", atom) evalerror("Invalid atom") end 1130 | stack = stack or {} 1131 | local v = nil 1132 | local e = atom 1133 | D("_run: next element is %1", e) 1134 | if base.type(e) == "number" or base.type(e) == "string" then 1135 | D("_run: direct value assignment for (%1)%2", base.type(e), e) 1136 | v = e 1137 | elseif isAtom( e, CONST ) then 1138 | D("_run: handling const %1", e.value) 1139 | v = e.value 1140 | elseif isAtom( e, BINOP ) then 1141 | D("_run: handling BINOP %1", e.op) 1142 | local v2 1143 | if e.op == 'and' or e.op == '&&' or e.op == 'or' or e.op == '||' then 1144 | v2 = e.rexpr 1145 | D("_run: logical lookahead is %1", v2) 1146 | elseif e.op == '.' then 1147 | v2 = e.rexpr 1148 | D("_run: subref lookahead is %1", v2) 1149 | else 1150 | v2 = runfetch( e.rexpr, ctx, stack ) -- something else, evaluate it now 1151 | end 1152 | local v1 1153 | if e.op == '=' then 1154 | -- Must be vref (can't assign to anything else). Special pop il lieu of fetch(). 1155 | v1 = e.lexpr 1156 | D("_run: assignment lookahead is %1", v1) 1157 | if not isAtom( v1, VREF ) then evalerror("Invalid assignment", e.pos) end 1158 | else 1159 | v1 = runfetch( e.lexpr, ctx, stack ) 1160 | end 1161 | D("_run: operands are %1, %2", v1, v2) 1162 | if e.op == '.' then 1163 | D("_run: descend to %1", v2) 1164 | if isNull(v1) then 1165 | if getOption( ctx, "nullderefnull" ) then 1166 | v = NULLATOM 1167 | else 1168 | evalerror("Can't dereference through null", e.pos) 1169 | end 1170 | else 1171 | if isAtom(v1) then evalerror("Invalid type in reference") end 1172 | if not check_operand(v1, "table") then evalerror("Cannot subreference a " .. base.type(v1), e.pos) end 1173 | if not isAtom( v2, VREF ) then evalerror("Invalid subreference", e.pos) end 1174 | if (v2.name or "") == "" and v2.index ~= nil then 1175 | -- Handle ['reference'] form of vref... name is in index 1176 | v2.name = runfetch( v2.index, ctx, stack ) 1177 | v2.index = nil 1178 | end 1179 | v = v1[v2.name] 1180 | if v2.index ~= nil then 1181 | -- Handle subscript in tree descent 1182 | if v == nil then evalerror("Can't index null", v2.pos) end 1183 | local ix = runfetch(v2.index, ctx, stack) 1184 | if ix == nil then evalerror("Subscript evaluation failed for " .. v2.name, v2.pos) end 1185 | v = v[ix] 1186 | if v == nil then 1187 | if getOption( ctx, "subscriptmissnull" ) then 1188 | v = NULLATOM 1189 | else 1190 | evalerror("Subscript out of range: " .. tostring(v2.name) .. "[" .. ix .. "]", v2.pos) 1191 | end 1192 | end 1193 | end 1194 | if v == nil then 1195 | -- Convert nil to NULL (not error, yet--depends on what expression does with it) 1196 | v = NULLATOM 1197 | end 1198 | end 1199 | elseif e.op == 'and' or e.op == '&&' then 1200 | if v1 == nil or not coerce(v1, "boolean") then 1201 | D("_run: shortcut and/&& op1 is false") 1202 | v = v1 -- shortcut lead expression if false (in "a and b", no need to eval b if a is false) 1203 | else 1204 | D("_run: op1 for and/&& is true, evaluate op2=%1", v2) 1205 | v = runfetch( v2, ctx, stack ) 1206 | end 1207 | elseif e.op == 'or' or e.op == '||' then 1208 | if v1 == nil or not coerce(v1, "boolean") then 1209 | D("_run: op1 for or/|| false, evaluate op2=%1", v2) 1210 | v = runfetch( v2, ctx, stack ) 1211 | else 1212 | D("_run: shortcut or/|| op1 is true") 1213 | v = v1 -- shortcut lead exp is true (in "a or b", no need to eval b if a is true) 1214 | end 1215 | elseif e.op == '..' then 1216 | -- String concatenation, explicit coercion to string for operands. 1217 | v = coerce(v1, "string") .. coerce(v2, "string") 1218 | elseif e.op == '+' then 1219 | -- Special case for +, which *can* concatenate strings. If both 1220 | -- operands can be coerced to number, add; otherwise concat as strings. 1221 | local cannum1 = base.type( v1 ) == "number" or base.type( v1 ) == "boolean" or tonumber( v1 ) ~= nil 1222 | local cannum2 = base.type( v2 ) == "number" or base.type( v2 ) == "boolean" or tonumber( v2 ) ~= nil 1223 | if cannum1 and cannum2 then 1224 | v = coerce(v1, "number") + coerce(v2, "number") 1225 | else 1226 | v = coerce(v1, "string") .. coerce(v2, "string") 1227 | end 1228 | elseif e.op == '-' then 1229 | v = coerce(v1, "number") - coerce(v2, "number") 1230 | elseif e.op == '*' then 1231 | v = coerce(v1, "number") * coerce(v2, "number") 1232 | elseif e.op == '/' then 1233 | v = coerce(v1, "number") / coerce(v2, "number") 1234 | elseif e.op == '%' then 1235 | v = coerce(v1, "number") % coerce(v2, "number") 1236 | elseif e.op == '&' then 1237 | -- If both operands are numbers, bitwise; otherwise boolean 1238 | if base.type(v1) ~= "number" or base.type(v2) ~= "number" then 1239 | v = coerce(v1, "boolean") and coerce(v2, "boolean") 1240 | else 1241 | v = bit.band( coerce(v1, "number"), coerce(v2, "number") ) 1242 | end 1243 | elseif e.op == '|' then 1244 | -- If both operands are numbers, bitwise; otherwise boolean 1245 | if base.type(v1) ~= "number" or base.type(v2) ~= "number" then 1246 | v = coerce(v1, "boolean") or coerce(v2, "boolean") 1247 | else 1248 | v = bit.bor( coerce(v1, "number"), coerce(v2, "number") ) 1249 | end 1250 | elseif e.op == '^' then 1251 | -- If both operands are numbers, bitwise; otherwise boolean 1252 | if base.type(v1) ~= "number" or base.type(v2) ~= "number" then 1253 | v = coerce(v1, "boolean") ~= coerce(v2, "boolean") 1254 | else 1255 | v = bit.bxor( coerce(v1, "number"), coerce(v2, "number") ) 1256 | end 1257 | elseif e.op == '<' then 1258 | if not check_operand(v1, {"number","string"}, v2) then evalerror("Invalid comparison (" 1259 | .. base.type(v1) .. e.op .. base.type(v2) .. ")", e.pos) end 1260 | v = v1 < v2 1261 | elseif e.op == '<=' then 1262 | if not check_operand(v1, {"number","string"}, v2) then evalerror("Invalid comparison (" 1263 | .. base.type(v1) .. e.op .. base.type(v2) .. ")", e.pos) end 1264 | v = v1 <= v2 1265 | elseif e.op == '>' then 1266 | if not check_operand(v1, {"number","string"}, v2) then evalerror("Invalid comparison (" 1267 | .. base.type(v1) .. e.op .. base.type(v2) .. ")", e.pos) end 1268 | v = v1 > v2 1269 | elseif e.op == '>=' then 1270 | if not check_operand(v1, {"number","string"}, v2) then evalerror("Invalid comparison (" 1271 | .. base.type(v1) .. e.op .. base.type(v2) .. ")", e.pos) end 1272 | v = v1 >= v2 1273 | elseif e.op == '==' then 1274 | if base.type(v1) == "boolean" or base.type(v2) == "boolean" then 1275 | v = coerce(v1, "boolean") == coerce(v2, "boolean") 1276 | elseif (base.type(v1) == "number" or base.type(v2) == "number") and isNumeric(v1) and isNumeric(v2) then 1277 | -- Either is number and both have valid numeric representation, treat both as numbers 1278 | -- That is 123 > "45" returns true 1279 | v = coerce(v1, "number") == coerce(v2, "number") 1280 | else 1281 | v = coerce(v1, "string") == coerce(v2, "string") 1282 | end 1283 | elseif e.op == '<>' or e.op == '!=' or e.op == '~=' then 1284 | if base.type(v1) == "boolean" or base.type(v2) == "boolean" then 1285 | v = coerce(v1, "boolean") == coerce(v2, "boolean") 1286 | elseif (base.type(v1) == "number" or base.type(v2) == "number") and isNumeric(v1) and isNumeric(v2) then 1287 | v = coerce(v1, "number") ~= coerce(v2, "number") 1288 | else 1289 | v = coerce(v1, "string") ~= coerce(v2, "string") 1290 | end 1291 | elseif e.op == '=' then 1292 | D("_run: making assignment to %1", v1) 1293 | -- Can't make assignment to reserved words 1294 | for j in pairs(reservedWords) do 1295 | if j == v1.name:lower() then evalerror("Can't assign to reserved word " .. j, e.pos) end 1296 | end 1297 | ctx.__lvars = ctx.__lvars or {} 1298 | if v1.index ~= nil then 1299 | -- Array/index assignment 1300 | if type(ctx.__lvars[v1.name]) ~= "table" then evalerror("Target is not an array ("..v1.name..")", e.pos) end 1301 | local ix = runfetch( v1.index, ctx, stack ) 1302 | D("_run: assignment to %1 with computed index %2", v1.name, ix) 1303 | if ix < 1 or type(ix) ~= "number" then evalerror("Invalid index ("..tostring(ix)..")", e.pos) end 1304 | ctx.__lvars[v1.name][ix] = v2 1305 | else 1306 | ctx.__lvars[v1.name] = v2 1307 | end 1308 | v = v2 1309 | else 1310 | error("Bug: binop parsed but not implemented by evaluator, binop=" .. e.op, 0) 1311 | end 1312 | elseif isAtom( e, UNOP ) then 1313 | -- Get the operand 1314 | D("_run: handling unop, stack has %1", stack) 1315 | v = runfetch( e.operand, ctx, stack ) 1316 | if v == nil then v = NULLATOM end 1317 | if e.op == '-' then 1318 | v = -coerce(v, "number") 1319 | elseif e.op == '+' then 1320 | -- noop 1321 | elseif e.op == '!' then 1322 | if base.type(v) == "number" then 1323 | v = bit.bnot(v) 1324 | else 1325 | v = not coerce(v, "boolean") 1326 | end 1327 | elseif e.op == '#' then 1328 | D("_run: # unop on %1", v) 1329 | local vt = base.type(v) 1330 | if vt == "string" then 1331 | v = #v 1332 | elseif vt == "table" then 1333 | v = xp_tlen( v ) 1334 | elseif isNull(v) then 1335 | v = 0 1336 | else 1337 | v = 1 1338 | end 1339 | else 1340 | error("Bug: unop parsed but not implemented by evaluator, unop=" .. e.op, 0) 1341 | end 1342 | elseif isAtom( e, FREF ) then 1343 | -- Function reference 1344 | D("_run: Handling function %1 with %2 args passed", e.name, #e.args) 1345 | if e.name == "if" then 1346 | -- Special-case the if() function, which evaluates only the sub-expression needed based on the result of the first argument. 1347 | -- This allows, for example, test for null before attempting to reference through it, as in if( x, x.name, "no name" ), 1348 | -- because arguments are normally evaluated prior to calling the function implementation, but this would cause "x.name" to 1349 | -- be attempted, which would fail and throw an error if x is null. 1350 | if #e.args < 2 then evalerror("if() requires two or three arguments", e.pos) end 1351 | local v1 = runfetch( e.args[1], ctx, stack ) 1352 | if v1 == nil or not coerce( v1, "boolean" ) then 1353 | -- False 1354 | if #e.args > 2 then 1355 | v = runfetch( e.args[3], ctx, stack ) 1356 | else 1357 | v = NULLATOM 1358 | end 1359 | else 1360 | -- True 1361 | v = runfetch( e.args[2], ctx, stack ) 1362 | end 1363 | elseif e.name == "iterate" then 1364 | if #e.args < 2 then evalerror("iterate() requires two or more arguments", e.pos) end 1365 | local v1 = runfetch( e.args[1], ctx, stack ) 1366 | v = {} 1367 | if v1 ~= nil and not isNull( v1 ) then 1368 | if type(v1) ~= "table" then evalerror("iterate() argument 1 is not array", e.pos) end 1369 | local v3 = '_' 1370 | if #e.args > 2 then 1371 | v3 = runfetch( e.args[3], ctx, stack ) 1372 | end 1373 | local iexp = isAtom( e.args[2], CONST ) and _comp( e.args[2].value, ctx ) or e.args[2] -- handle string as expression 1374 | -- if not isAtom( iexp ) then evalerror("iterate() arg 2 must be expression or string containing expression", e.pos) end 1375 | ctx.__lvars = ctx.__lvars or {} 1376 | for _,xa in ipairs( v1 ) do 1377 | ctx.__lvars[v3] = xa 1378 | local xv = runfetch( iexp, ctx, stack ) 1379 | if xv ~= nil and not isNull( xv ) then 1380 | table.insert( v, xv ) 1381 | end 1382 | end 1383 | end 1384 | elseif e.name == "map" then 1385 | if #e.args < 1 then evalerror("map() requires one or more arguments", e.pos) end 1386 | local v1 = runfetch( e.args[1], ctx, stack ) 1387 | v = {} 1388 | if v1 ~= nil and not isNull( v1 ) then 1389 | if type(v1) ~= "table" then evalerror("map() argument 1 is not array", e.pos) end 1390 | local v3 = '_' 1391 | if #e.args > 2 then 1392 | v3 = runfetch( e.args[3], ctx, stack ) 1393 | end 1394 | local iexp 1395 | if #e.args > 1 then 1396 | iexp = isAtom( e.args[2], CONST ) and _comp( e.args[2].value, ctx ) or e.args[2] -- handle string as expression 1397 | -- if not isAtom( iexp ) then evalerror("iterate() arg 2 must be expression or string containing expression", e.pos) end 1398 | else 1399 | iexp = _comp( "__", ctx ) -- default index to pivot array 1400 | end 1401 | ctx.__lvars = ctx.__lvars or {} 1402 | for k,xa in ipairs( v1 ) do 1403 | if not isNull( xa ) then 1404 | ctx.__lvars[v3] = xa 1405 | ctx.__lvars['__'] = k 1406 | local xv = runfetch( iexp, ctx, stack ) 1407 | if xv ~= nil and not isNull( xv ) then 1408 | v[tostring(xa)] = xv 1409 | end 1410 | end 1411 | end 1412 | end 1413 | else 1414 | -- Parse our arguments and put each on the stack; push them in reverse so they pop correctly (first to pop is first passed) 1415 | local v1, argv 1416 | local argc = #e.args 1417 | argv = {} 1418 | for n=1,argc do 1419 | v = e.args[n] 1420 | D("_run: evaluate function argument %1: %2", n, v) 1421 | v1 = runfetch( v, ctx, stack) 1422 | if v1 == nil then v1 = NULLATOM end 1423 | D("_run: adding argument result %1", v1) 1424 | argv[n] = v1 1425 | end 1426 | -- Locate the implementation 1427 | local impl = nil 1428 | if nativeFuncs[e.name] then 1429 | D("_run: found native func %1", nativeFuncs[e.name]) 1430 | impl = nativeFuncs[e.name].impl 1431 | if (argc < nativeFuncs[e.name].nargs) then evalerror("Insufficient arguments to " .. e.name .. "(), need " .. nativeFuncs[e.name].nargs .. ", got " .. argc, e.pos) end 1432 | end 1433 | if impl == nil and ctx['__functions'] then 1434 | impl = ctx['__functions'][e.name] 1435 | D("_run: context __functions provides implementation") 1436 | end 1437 | if impl == nil then 1438 | D("_run: context provides DEPRECATED-STYLE implementation") 1439 | impl = ctx[e.name] 1440 | end 1441 | if impl == nil then evalerror("Unrecognized function: " .. e.name, e.pos) end 1442 | if base.type(impl) ~= "function" then evalerror("Reference is not a function: " .. e.name, e.pos) end 1443 | -- Run the implementation 1444 | local status 1445 | D("_run: calling %1 with args=%2", e.name, argv) 1446 | argv.__context = ctx -- trickery 1447 | status, v = pcall(impl, argv) 1448 | D("_run: finished %1() call, status=%2, result=%3", e.name, status, v) 1449 | if not status then 1450 | if base.type(v) == "table" and v.__source == "luaxp" then 1451 | v.location = e.pos 1452 | error(v) -- that one of our errors, just pass along 1453 | end 1454 | error("Execution of function " .. e.name .. "() threw an error: " .. tostring(v)) 1455 | end 1456 | end 1457 | elseif isAtom( e, VREF ) then 1458 | D("_run: handling vref, name=%1, push to stack for later eval", e.name) 1459 | v = deepcopy(e) -- we're going to push the VREF directly (e.g. pushing atom to stack!) 1460 | else 1461 | error("Bug: invalid atom type in parse tree: " .. tostring(e.__type), 0) 1462 | end 1463 | 1464 | -- Push result to stack 1465 | D("_run: pushing result to stack: %1", v) 1466 | if v == 0 then v = 0 end -- Huh? Well... long story. Resolve the inconsistency of -0 in Lua. See issue #4. 1467 | table.insert(stack, v) 1468 | D("_run: finished, stack has %1: %2", #stack, stack) 1469 | return true 1470 | end 1471 | 1472 | -- PUBLIC METHODS 1473 | 1474 | -- Compile the expression (public method) 1475 | function _M.compile( expressionString ) 1476 | local s,v,n -- n??? 1477 | s,v,n = pcall(_comp, expressionString) 1478 | if s then 1479 | return { rpn = v, source = expressionString } 1480 | else 1481 | return nil, v 1482 | end 1483 | end 1484 | 1485 | -- Public method to execute compiled expression. Accepts a context (ctx) 1486 | function _M.run( compiledExpression, executionContext ) 1487 | executionContext = executionContext or {} 1488 | if (compiledExpression == nil or compiledExpression.rpn == nil or base.type(compiledExpression.rpn) ~= "table") then return nil end 1489 | local stack = {} 1490 | local status, val = pcall(_run, compiledExpression.rpn, executionContext, stack) 1491 | if #stack==0 or not status then return nil, val end 1492 | -- Put through fetch() because we may leave VREF atoms on the stack for last 1493 | status, val = pcall( fetch, stack, executionContext ) -- return first element. Maybe return multiple some day??? 1494 | if not status then return nil, val end 1495 | return val 1496 | end 1497 | 1498 | -- Public convenience method to compile and run and expression. 1499 | function _M.evaluate( expressionString, executionContext ) 1500 | local r,m = _M.compile( expressionString ) 1501 | if r == nil then return r,m end -- return error as we got it 1502 | return _M.run( r, executionContext ) -- and directly return whatever run() wants to return 1503 | end 1504 | 1505 | -- Special exports 1506 | _M.dump = dump 1507 | _M.isNull = isNull 1508 | _M.coerce = coerce 1509 | _M.NULL = NULLATOM 1510 | _M.null = NULLATOM 1511 | _M.evalerror = evalerror 1512 | 1513 | return _M 1514 | -------------------------------------------------------------------------------- /test/testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "full": 1, 3 | "temperature": "F", 4 | "mode": 1, 5 | "sections": [{ 6 | "name": "My Home", 7 | "id": 1 8 | }], 9 | "rooms": [{ 10 | "name": "Breakfast Area", 11 | "id": 8, 12 | "section": 1 13 | }, { 14 | "name": "Dining Room", 15 | "id": 12, 16 | "section": 1 17 | }, { 18 | "name": "Family Room", 19 | "id": 11, 20 | "section": 1 21 | }, { 22 | "name": "Foyer", 23 | "id": 10, 24 | "section": 1 25 | }, { 26 | "name": "Front Porch", 27 | "id": 14, 28 | "section": 1 29 | }, { 30 | "name": "Guest Suite", 31 | "id": 15, 32 | "section": 1 33 | }, { 34 | "name": "Gym", 35 | "id": 5, 36 | "section": 1 37 | }, { 38 | "name": "Ian's Suite", 39 | "id": 16, 40 | "section": 1 41 | }, { 42 | "name": "Jack n Jill Bath", 43 | "id": 17, 44 | "section": 1 45 | }, { 46 | "name": "Kitchen", 47 | "id": 9, 48 | "section": 1 49 | }, { 50 | "name": "Lab", 51 | "id": 1, 52 | "section": 1 53 | }, { 54 | "name": "Laundry", 55 | "id": 18, 56 | "section": 1 57 | }, { 58 | "name": "Lounge", 59 | "id": 3, 60 | "section": 1 61 | }, { 62 | "name": "Master Bath", 63 | "id": 20, 64 | "section": 1 65 | }, { 66 | "name": "Master Bedroom", 67 | "id": 19, 68 | "section": 1 69 | }, { 70 | "name": "Media Room", 71 | "id": 2, 72 | "section": 1 73 | }, { 74 | "name": "Office", 75 | "id": 13, 76 | "section": 1 77 | }, { 78 | "name": "Play Room", 79 | "id": 22, 80 | "section": 1 81 | }, { 82 | "name": "Ryan's Room", 83 | "id": 21, 84 | "section": 1 85 | }, { 86 | "name": "Terrace Bath", 87 | "id": 4, 88 | "section": 1 89 | }, { 90 | "name": "Terrace Office", 91 | "id": 6, 92 | "section": 1 93 | }, { 94 | "name": "Terrace Patio", 95 | "id": 7, 96 | "section": 1 97 | }, { 98 | "name": "Virtual Devices", 99 | "id": 23, 100 | "section": 1 101 | }], 102 | "scenes": [{ 103 | "active": 0, 104 | "name": "All Off", 105 | "id": 25, 106 | "room": 0 107 | }, { 108 | "active": 0, 109 | "name": "Cat Box Off", 110 | "id": 37, 111 | "room": 18 112 | }, { 113 | "active": 1, 114 | "name": "Cat Box On", 115 | "id": 36, 116 | "room": 18 117 | }, { 118 | "active": 0, 119 | "name": "Ceiling Fans Off", 120 | "id": 42, 121 | "room": 0 122 | }, { 123 | "active": 0, 124 | "name": "Go To Basement", 125 | "id": 23, 126 | "room": 0 127 | }, { 128 | "active": 1, 129 | "name": "Ian Bedroom Off", 130 | "id": 24, 131 | "room": 16 132 | }, { 133 | "active": 0, 134 | "name": "Indoor Off", 135 | "id": 20, 136 | "room": 0 137 | }, { 138 | "active": 0, 139 | "name": "Kitchen All On", 140 | "id": 16, 141 | "room": 9 142 | }, { 143 | "active": 1, 144 | "name": "Kitchen Off", 145 | "id": 15, 146 | "room": 9 147 | }, { 148 | "active": 0, 149 | "name": "Kitchen Task", 150 | "id": 17, 151 | "room": 9 152 | }, { 153 | "active": 0, 154 | "name": "Lounge All", 155 | "id": 11, 156 | "room": 3 157 | }, { 158 | "active": 1, 159 | "name": "Lounge Off", 160 | "id": 10, 161 | "room": 3 162 | }, { 163 | "active": 0, 164 | "name": "Main Level Off", 165 | "id": 33, 166 | "room": 0 167 | }, { 168 | "active": 1, 169 | "name": "Master Bath Off", 170 | "id": 21, 171 | "room": 20 172 | }, { 173 | "active": 0, 174 | "name": "Master Bedroom Ambient", 175 | "id": 31, 176 | "room": 19 177 | }, { 178 | "active": 0, 179 | "name": "Master Suite Off", 180 | "id": 32, 181 | "room": 19 182 | }, { 183 | "active": 1, 184 | "name": "Morning Off", 185 | "id": 9, 186 | "room": 0 187 | }, { 188 | "active": 0, 189 | "name": "Outdoor All", 190 | "id": 14, 191 | "room": 0 192 | }, { 193 | "active": 1, 194 | "name": "Outdoor Off", 195 | "id": 13, 196 | "room": 0 197 | }, { 198 | "active": 0, 199 | "name": "Panic On", 200 | "id": 1, 201 | "room": 0 202 | }, { 203 | "active": 1, 204 | "name": "Reset PLEG Timers", 205 | "id": 41, 206 | "room": 0 207 | }, { 208 | "active": 0, 209 | "name": "Ryan Bedroom Off", 210 | "id": 43, 211 | "room": 21 212 | }, { 213 | "active": 0, 214 | "name": "Set Away", 215 | "id": 18, 216 | "room": 0 217 | }, { 218 | "active": 1, 219 | "name": "Set Home", 220 | "id": 19, 221 | "room": 0 222 | }, { 223 | "active": 0, 224 | "name": "Sunset Off", 225 | "id": 7, 226 | "room": 0 227 | }, { 228 | "active": 0, 229 | "name": "Sunset On", 230 | "id": 6, 231 | "room": 0 232 | }, { 233 | "active": 1, 234 | "name": "Terrace Bath Off", 235 | "id": 28, 236 | "room": 4 237 | }, { 238 | "active": 0, 239 | "name": "Terrace Level Off", 240 | "id": 34, 241 | "room": 0 242 | }, { 243 | "active": 0, 244 | "name": "Terrace Office All", 245 | "id": 51, 246 | "room": 6 247 | }, { 248 | "active": 1, 249 | "name": "Terrace Office Off", 250 | "id": 12, 251 | "room": 6 252 | }, { 253 | "active": 0, 254 | "name": "Test1", 255 | "id": 52, 256 | "room": 0 257 | }, { 258 | "active": 0, 259 | "name": "Test2", 260 | "id": 53, 261 | "room": 8 262 | }, { 263 | "active": 0, 264 | "name": "Theater Full", 265 | "id": 2, 266 | "room": 2 267 | }, { 268 | "active": 0, 269 | "name": "Theater Movie", 270 | "id": 5, 271 | "room": 2 272 | }, { 273 | "active": 1, 274 | "name": "Theater Off", 275 | "id": 3, 276 | "room": 2 277 | }, { 278 | "active": 0, 279 | "name": "Theater Partial", 280 | "id": 4, 281 | "room": 2 282 | }, { 283 | "active": 0, 284 | "name": "Upper Level Off", 285 | "id": 35, 286 | "room": 0 287 | }, { 288 | "active": 0, 289 | "name": "Weekday Morning On", 290 | "id": 8, 291 | "room": 0 292 | }, { 293 | "active": 0, 294 | "name": "Xmas Off", 295 | "id": 48, 296 | "room": 0 297 | }, { 298 | "active": 1, 299 | "name": "Xmas On", 300 | "id": 49, 301 | "room": 0 302 | }, { 303 | "active": 0, 304 | "name": "_do_bridge_scene", 305 | "id": 29, 306 | "room": 10 307 | }, { 308 | "active": 0, 309 | "name": "_do_entry_scene", 310 | "id": 27, 311 | "room": 10 312 | }, { 313 | "active": 0, 314 | "name": "_do_lounge_sc", 315 | "id": 46, 316 | "room": 3 317 | }, { 318 | "active": 0, 319 | "name": "_do_nest_away", 320 | "id": 39, 321 | "room": 0 322 | }, { 323 | "active": 0, 324 | "name": "_do_nest_home", 325 | "id": 40, 326 | "room": 0 327 | }, { 328 | "active": 0, 329 | "name": "_do_panic_scene", 330 | "id": 45, 331 | "room": 0 332 | }], 333 | "devices": [{ 334 | "name": "6 in 1 Multisensor (humidity)", 335 | "altid": "m5", 336 | "id": 209, 337 | "category": 16, 338 | "subcategory": 0, 339 | "room": 2, 340 | "parent": 206, 341 | "humidity": "60" 342 | }, { 343 | "name": "6 in 1 Multisensor (humidity) 1", 344 | "altid": "m5", 345 | "id": 229, 346 | "category": 16, 347 | "subcategory": 0, 348 | "room": 16, 349 | "parent": 225, 350 | "humidity": "54" 351 | }, { 352 | "name": "6 in 1 Multisensor (light)", 353 | "altid": "m3", 354 | "id": 208, 355 | "category": 18, 356 | "subcategory": 0, 357 | "room": 2, 358 | "parent": 206, 359 | "light": "0" 360 | }, { 361 | "name": "6 in 1 Multisensor (light) 1", 362 | "altid": "m3", 363 | "id": 228, 364 | "category": 18, 365 | "subcategory": 0, 366 | "room": 16, 367 | "parent": 225, 368 | "light": "6" 369 | }, { 370 | "name": "6 in 1 Multisensor (temperature)", 371 | "altid": "m1", 372 | "id": 207, 373 | "category": 17, 374 | "subcategory": 0, 375 | "room": 2, 376 | "parent": 206, 377 | "temperature": "74.0" 378 | }, { 379 | "name": "6 in 1 Multisensor (temperature) 1", 380 | "altid": "m1", 381 | "id": 226, 382 | "category": 17, 383 | "subcategory": 0, 384 | "room": 16, 385 | "parent": 225, 386 | "temperature": "75.2" 387 | }, { 388 | "name": "6 in 1 Multisensor (uv)", 389 | "altid": "m27", 390 | "id": 210, 391 | "category": 28, 392 | "subcategory": 0, 393 | "room": 2, 394 | "parent": 206, 395 | "armed": "0", 396 | "armedtripped": "0", 397 | "light": "0" 398 | }, { 399 | "name": "6 in 1 Multisensor (uv) 1", 400 | "altid": "m27", 401 | "id": 227, 402 | "category": 28, 403 | "subcategory": 0, 404 | "room": 16, 405 | "parent": 225, 406 | "armed": "0", 407 | "armedtripped": "0", 408 | "light": "0" 409 | }, { 410 | "name": "Aeon Range Extender", 411 | "altid": "103", 412 | "id": 212, 413 | "category": 11, 414 | "subcategory": 2, 415 | "room": 12, 416 | "parent": 1, 417 | "status": "0", 418 | "armed": "0", 419 | "armedtripped": "0" 420 | }, { 421 | "name": "Bar Back", 422 | "altid": "18", 423 | "id": 20, 424 | "category": 2, 425 | "subcategory": 0, 426 | "room": 3, 427 | "parent": 1, 428 | "status": "0", 429 | "level": "0" 430 | }, { 431 | "name": "Bar Sink", 432 | "altid": "19", 433 | "id": 21, 434 | "category": 2, 435 | "subcategory": 0, 436 | "room": 3, 437 | "parent": 1, 438 | "status": "0", 439 | "level": "0" 440 | }, { 441 | "name": "Bar Top", 442 | "altid": "17", 443 | "id": 19, 444 | "category": 2, 445 | "subcategory": 0, 446 | "room": 3, 447 | "parent": 1, 448 | "status": "0", 449 | "level": "0" 450 | }, { 451 | "name": "Bottom Stair SC1", 452 | "altid": "36", 453 | "id": 44, 454 | "category": 14, 455 | "subcategory": 0, 456 | "room": 8, 457 | "parent": 1, 458 | "commFailure": "0", 459 | "status": "0" 460 | }, { 461 | "name": "Breakfast Chandelier", 462 | "altid": "28", 463 | "id": 36, 464 | "category": 2, 465 | "subcategory": 0, 466 | "room": 8, 467 | "parent": 1, 468 | "status": "0", 469 | "level": "0" 470 | }, { 471 | "name": "Bridge Recept", 472 | "altid": "102", 473 | "id": 93, 474 | "category": 3, 475 | "subcategory": 0, 476 | "room": 10, 477 | "parent": 1, 478 | "status": "1" 479 | }, { 480 | "name": "Bridge Recessed", 481 | "altid": "68", 482 | "id": 92, 483 | "category": 2, 484 | "subcategory": 0, 485 | "room": 10, 486 | "parent": 1, 487 | "status": "0", 488 | "level": "0" 489 | }, { 490 | "name": "Cat Box Switch", 491 | "altid": "109", 492 | "id": 76, 493 | "category": 3, 494 | "subcategory": 1, 495 | "room": 18, 496 | "parent": 1, 497 | "status": "1" 498 | }, { 499 | "name": "Ceiling Fan Light", 500 | "altid": "38", 501 | "id": 47, 502 | "category": 2, 503 | "subcategory": 0, 504 | "room": 11, 505 | "parent": 1, 506 | "status": "0", 507 | "level": "0" 508 | }, { 509 | "name": "Ceiling Fan Motor", 510 | "altid": "39", 511 | "id": 48, 512 | "category": 3, 513 | "subcategory": 0, 514 | "room": 11, 515 | "parent": 1, 516 | "status": "1" 517 | }, { 518 | "name": "Craft Room Lights", 519 | "altid": "94", 520 | "id": 166, 521 | "category": 3, 522 | "subcategory": 0, 523 | "room": 20, 524 | "parent": 1, 525 | "status": "0" 526 | }, { 527 | "name": "Deck Flood", 528 | "altid": "181", 529 | "id": 42, 530 | "category": 3, 531 | "subcategory": 3, 532 | "room": 9, 533 | "parent": 1, 534 | "status": "0", 535 | "commFailure": "0" 536 | }, { 537 | "name": "Deck High Flood", 538 | "altid": "58", 539 | "id": 81, 540 | "category": 3, 541 | "subcategory": 0, 542 | "room": 19, 543 | "parent": 1, 544 | "status": "0", 545 | "commFailure": "0" 546 | }, { 547 | "name": "Deus Ex Machina II -", 548 | "altid": "", 549 | "id": 170, 550 | "category": 0, 551 | "subcategory": -1, 552 | "room": 23, 553 | "parent": 0, 554 | "enabled": "0", 555 | "state": "0", 556 | "housemodes": "20", 557 | "lightsout": "1342", 558 | "devices": "36,51=52,47,71=51,63=52,27,70,103,37,164,19,82,90,166,77,79=52,10=53,53,99,95", 559 | "status": "0" 560 | }, { 561 | "name": "Dining Chandelier", 562 | "altid": "40", 563 | "id": 51, 564 | "category": 2, 565 | "subcategory": 0, 566 | "room": 12, 567 | "parent": 1, 568 | "status": "0", 569 | "level": "0" 570 | }, { 571 | "name": "Dining SC1", 572 | "altid": "42", 573 | "id": 52, 574 | "category": 14, 575 | "subcategory": 0, 576 | "room": 12, 577 | "parent": 1 578 | }, { 579 | "name": "Event Watcher", 580 | "altid": "", 581 | "id": 140, 582 | "category": 0, 583 | "subcategory": -1, 584 | "room": 23, 585 | "parent": 0 586 | }, { 587 | "name": "Everspring ST814-2", 588 | "altid": "92", 589 | "id": 133, 590 | "category": 11, 591 | "subcategory": 0, 592 | "room": 1, 593 | "parent": 1, 594 | "batterylevel": "80", 595 | "temperature": "70.1", 596 | "humidity": "59", 597 | "lasttrip": "1501217441", 598 | "tripped": "0", 599 | "armed": "0", 600 | "armedtripped": "0" 601 | }, { 602 | "name": "Far Recessed", 603 | "altid": "133", 604 | "id": 31, 605 | "category": 2, 606 | "subcategory": 3, 607 | "room": 6, 608 | "parent": 1, 609 | "status": "0", 610 | "level": "0" 611 | }, { 612 | "name": "Foyer Chandelier", 613 | "altid": "50", 614 | "id": 71, 615 | "category": 2, 616 | "subcategory": 0, 617 | "room": 10, 618 | "parent": 1, 619 | "status": "0", 620 | "level": "0", 621 | "commFailure": "0" 622 | }, { 623 | "name": "Foyer Receptacle", 624 | "altid": "101", 625 | "id": 45, 626 | "category": 3, 627 | "subcategory": 0, 628 | "room": 10, 629 | "parent": 1, 630 | "status": "1" 631 | }, { 632 | "name": "Foyer Window", 633 | "altid": "169", 634 | "id": 59, 635 | "category": 3, 636 | "subcategory": 0, 637 | "room": 10, 638 | "parent": 1, 639 | "status": "0" 640 | }, { 641 | "name": "Front Landscape", 642 | "altid": "168", 643 | "id": 65, 644 | "category": 3, 645 | "subcategory": 0, 646 | "room": 14, 647 | "parent": 1, 648 | "status": "0", 649 | "commFailure": "0" 650 | }, { 651 | "name": "Front Spots", 652 | "altid": "6", 653 | "id": 7, 654 | "category": 2, 655 | "subcategory": 0, 656 | "room": 2, 657 | "parent": 1, 658 | "status": "0", 659 | "level": "0" 660 | }, { 661 | "name": "Garage Door Sconce", 662 | "altid": "194", 663 | "id": 168, 664 | "category": 3, 665 | "subcategory": 3, 666 | "room": 8, 667 | "parent": 1, 668 | "status": "0" 669 | }, { 670 | "name": "Garage Lights", 671 | "altid": "27", 672 | "id": 35, 673 | "category": 3, 674 | "subcategory": 0, 675 | "room": 8, 676 | "parent": 1, 677 | "status": "0", 678 | "commFailure": "0" 679 | }, { 680 | "name": "Guest Bath Fan", 681 | "altid": "46", 682 | "id": 61, 683 | "category": 3, 684 | "subcategory": 0, 685 | "room": 15, 686 | "parent": 1, 687 | "status": "0" 688 | }, { 689 | "name": "Guest Bath Light", 690 | "altid": "47", 691 | "id": 62, 692 | "category": 2, 693 | "subcategory": 0, 694 | "room": 15, 695 | "parent": 1, 696 | "status": "0", 697 | "level": "0", 698 | "commFailure": "0", 699 | "armed": "0", 700 | "armedtripped": "0" 701 | }, { 702 | "name": "Guest Bath PLTS", 703 | "altid": "", 704 | "id": 151, 705 | "category": 0, 706 | "subcategory": -1, 707 | "room": 15, 708 | "parent": 0, 709 | "iconstate": "0", 710 | "status": "0", 711 | "state": "0", 712 | "armed": "1", 713 | "interval": "1800", 714 | "interval2": "60", 715 | "resetstate": "0", 716 | "switchmode": "1", 717 | "offcondition": "1501870284", 718 | "conditionrepeat": "{}", 719 | "objectstatusmap": "{{name = 'Arm', state = nil, seq = 0, oseq = 1462476636.0177},{name = 'Bypass', state = nil, seq = 0, oseq = 1462476636.0179},{name = 'Reset', state = nil, seq = 0, oseq = 1462476636.018},{name = 'Trigger', state = false, seq = 1501868978.5297, oseq = 1501869068.7398},{name = 'Trigger2', state = nil, seq = 0, oseq = 1462476636.0183},{name = 'Restart', state = nil, seq = 0, oseq = 1462476636.0184},{name = 'Restart2', state = nil, seq = 0, oseq = 1462476636.0185},{name = 'On', state = nil, seq = 0, oseq = 1462476636.0187},{name = 'Off', state = nil, seq = 0, oseq = 1462476636.0188},{name = 'fan_on', state = false, seq = 1501862668.7581, oseq = 1501862828.698},{name = 'light_on', state = false, seq = 1501868978.5276, oseq = 1501869068.7377},{name = 'light_hall', state = false, seq = 1500410719.3956, oseq = 1500412053.8276},}", 720 | "triggers": "[{'name':'fan_on','device':'61','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_on','device':'62','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_hall','device':'64','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]}]", 721 | "actions": "{'Off':[{'delay':0,'actions':[{'device':62,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]},{'device':61,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]},{'device':64,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}]}" 722 | }, { 723 | "name": "Guest Hall Light", 724 | "altid": "208", 725 | "id": 64, 726 | "category": 3, 727 | "subcategory": 3, 728 | "room": 15, 729 | "parent": 1, 730 | "status": "0" 731 | }, { 732 | "name": "Guest Room Light", 733 | "altid": "48", 734 | "id": 63, 735 | "category": 2, 736 | "subcategory": 0, 737 | "room": 15, 738 | "parent": 1, 739 | "status": "0", 740 | "level": "0" 741 | }, { 742 | "name": "Gym Door Sconces", 743 | "altid": "25", 744 | "id": 28, 745 | "category": 2, 746 | "subcategory": 0, 747 | "room": 7, 748 | "parent": 1, 749 | "status": "0", 750 | "level": "0" 751 | }, { 752 | "name": "Gym Lights", 753 | "altid": "24", 754 | "id": 27, 755 | "category": 2, 756 | "subcategory": 0, 757 | "room": 5, 758 | "parent": 1, 759 | "status": "0", 760 | "level": "0" 761 | }, { 762 | "name": "Gym Motion", 763 | "altid": "91", 764 | "id": 132, 765 | "category": 4, 766 | "subcategory": 3, 767 | "room": 5, 768 | "parent": 1, 769 | "armedtripped": "0", 770 | "lasttrip": "1501602182", 771 | "tripped": "0", 772 | "armed": "0", 773 | "batterylevel": "100", 774 | "status": "1" 775 | }, { 776 | "name": "Gym PLTS", 777 | "altid": "", 778 | "id": 148, 779 | "category": 0, 780 | "subcategory": -1, 781 | "room": 5, 782 | "parent": 0, 783 | "iconstate": "0", 784 | "status": "0", 785 | "state": "0", 786 | "armed": "1", 787 | "interval": "3600", 788 | "interval2": "60", 789 | "resetstate": "0", 790 | "switchmode": "1", 791 | "offcondition": "1501380083", 792 | "conditionrepeat": "{'Restart':true}", 793 | "objectstatusmap": "{{name = 'Arm', state = nil, seq = 0, oseq = 1462474815.3822},{name = 'Bypass', state = nil, seq = 0, oseq = 1462474815.3824},{name = 'Reset', state = nil, seq = 0, oseq = 1462474815.3825},{name = 'Trigger', state = false, seq = 1501376484.0071, oseq = 1501376487.677},{name = 'Trigger2', state = nil, seq = 0, oseq = 1462474815.3828},{name = 'Restart', state = false, seq = 1501602175.6148, oseq = 1501602182.285},{name = 'Restart2', state = nil, seq = 0, oseq = 1462474815.383},{name = 'On', state = nil, seq = 0, oseq = 1462474815.3832},{name = 'Off', state = nil, seq = 0, oseq = 1462474815.3833},{name = 'light_on', state = false, seq = 1501376484.0057, oseq = 1501376487.6756},{name = 'motion', state = false, seq = 1501602175.6129, oseq = 1501602182.2831},}", 794 | "triggers": "[{'name':'light_on','device':'27','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'motion','device':'132','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]}]", 795 | "actions": "{'Off':[{'delay':0,'actions':[{'device':27,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'0'}]}]}]}" 796 | }, { 797 | "name": "Hall Light", 798 | "altid": "20", 799 | "id": 22, 800 | "category": 3, 801 | "subcategory": 0, 802 | "room": 3, 803 | "parent": 1, 804 | "status": "0" 805 | }, { 806 | "name": "Headboard", 807 | "altid": "202", 808 | "id": 77, 809 | "category": 3, 810 | "subcategory": 0, 811 | "room": 19, 812 | "parent": 1, 813 | "status": "1" 814 | }, { 815 | "name": "Humidity Control PLG", 816 | "altid": "", 817 | "id": 407, 818 | "category": 0, 819 | "subcategory": -1, 820 | "room": 0, 821 | "parent": 0, 822 | "armed": "0", 823 | "conditionsatisfied": "None", 824 | "conditionrepeat": "{}", 825 | "objectstatusmap": "{{name = 'c1', state = true, seq = 1501679778.0069, oseq = 1501599708.1015},{name = 'c2', state = false, seq = 0, oseq = 0},{name = 't1', state = true, seq = 1501679778.0056, oseq = 1501599708.1006},{name = 't2', state = false, seq = 1501634725.0255, oseq = 1501635626.8768},{name = 't3', state = false, seq = 0, oseq = 0},}", 826 | "triggers": "[{'device':'147','name':'t1','service':'urn:micasaverde-com:serviceId:HumiditySensor1','template':'1','args':[{'op':'>','name':'CurrentLevel','value':'50'}]},{'device':'147','name':'t2','service':'urn:micasaverde-com:serviceId:HumiditySensor1','template':'2','args':[{'op':'<','name':'CurrentLevel','value':'45'}]},{'device':'144','name':'t3','service':'urn:upnp-org:serviceId:TemperatureSetpoint1_Cool','template':'5','args':[{'op':'<','name':'CurrentSetpoint','value':'68'}]}]", 827 | "actions": "{'lua_OutputVariableNames':{},'c1':[{'delay':0,'actions':[{'device':'144','service':'urn:upnp-org:serviceId:HVAC_UserOperatingMode1','action':'SetModeTarget','arguments':[{'name':'NewModeTarget','value':'Cool'}]},{'device':'144','service':'urn:upnp-org:serviceId:TemperatureSetpoint1_Cool','action':'SetCurrentSetpoint','arguments':[{'name':'NewCurrentSetpoint','value':'66'}]}]}],'c2':[{'delay':0,'actions':[{'device':'144','service':'urn:upnp-org:serviceId:TemperatureSetpoint1_Cool','action':'SetCurrentSetpoint','arguments':[{'name':'NewCurrentSetpoint','value':'75'}]}]}]}" 828 | }, { 829 | "name": "Ian Bath Fan", 830 | "altid": "108", 831 | "id": 218, 832 | "category": 3, 833 | "subcategory": 0, 834 | "room": 16, 835 | "parent": 1, 836 | "status": "0", 837 | "commFailure": "0" 838 | }, { 839 | "name": "Ian Bath Vanity", 840 | "altid": "49", 841 | "id": 67, 842 | "category": 2, 843 | "subcategory": 0, 844 | "room": 16, 845 | "parent": 1, 846 | "status": "0", 847 | "level": "0" 848 | }, { 849 | "name": "Ian Bedroom Light", 850 | "altid": "153", 851 | "id": 70, 852 | "category": 2, 853 | "subcategory": 3, 854 | "room": 16, 855 | "parent": 1, 856 | "status": "0", 857 | "level": "0" 858 | }, { 859 | "name": "Ian Closet Light", 860 | "altid": "147", 861 | "id": 66, 862 | "category": 3, 863 | "subcategory": 3, 864 | "room": 16, 865 | "parent": 1, 866 | "status": "0" 867 | }, { 868 | "name": "Ian Shower Light", 869 | "altid": "110", 870 | "id": 219, 871 | "category": 3, 872 | "subcategory": 0, 873 | "room": 16, 874 | "parent": 1, 875 | "status": "0", 876 | "commFailure": "0" 877 | }, { 878 | "name": "Ian Suite PLEG", 879 | "altid": "", 880 | "id": 153, 881 | "category": 0, 882 | "subcategory": -1, 883 | "room": 16, 884 | "parent": 0, 885 | "armed": "1", 886 | "conditionsatisfied": "None", 887 | "conditionrepeat": "{'bedroom_extend':true}", 888 | "objectstatusmap": "{{name = 'fan_timer', state = false, seq = 1501696048.322, oseq = 1501697848.1005},{name = 'closet_timer', state = false, seq = 1500996112.3055, oseq = 1500996712.0242},{name = 'light_bath', state = false, seq = 1501813228.2413, oseq = 1501814728.2383},{name = 'bedroom_start', state = false, seq = 1501873965.2955, oseq = 1501875435.235},{name = 'closet_start', state = false, seq = 1500996112.2335, oseq = 1500996712.3073},{name = 'bath_start', state = false, seq = 1501813228.244, oseq = 1501814728.2404},{name = 'fan_start', state = false, seq = 1501696048.2303, oseq = 1501697848.5286},{name = 'fan_expire', state = true, seq = 1501697848.1158, oseq = 1501696048.3272},{name = 'closet_expire', state = true, seq = 1500996712.0298, oseq = 1500996112.3127},{name = 'bedroom_expire', state = false, seq = 1501871664.0321, oseq = 1501873965.3444},{name = 'bath_expire', state = true, seq = 1501816828.1188, oseq = 1501813228.2924},{name = 'bedroom_extend', state = false, seq = 1501875062.5206, oseq = 1501877885.9564},{name = 'fan_on', state = false, seq = 1501696048.2261, oseq = 1501697848.5244},{name = 'light_vanity', state = false, seq = 1501813228.2396, oseq = 1501814728.2366},{name = 'light_bedroom', state = false, seq = 1501873965.2934, oseq = 1501875435.2328},{name = 'light_closet', state = false, seq = 1500996112.2308, oseq = 1500996712.3046},{name = 'light_shower', state = false, seq = 1501672275.1686, oseq = 1501673775.2388},{name = 'motion', state = false, seq = 1501877661.0433, oseq = 1501877885.9485},{name = 'bath_timer', state = false, seq = 1501813228.2847, oseq = 1501816828.1012},{name = 'bedroom_timer', state = true, seq = 1501875062.5516, oseq = 1501871664.0257},}", 889 | "triggers": "[{'name':'fan_on','device':'218','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'op':'=','name':'Status','value':'1'}]},{'name':'light_bedroom','device':'70','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_closet','device':'66','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_shower','device':'219','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'op':'=','name':'Status','value':'1'}]},{'name':'light_vanity','device':'67','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'motion','device':'225','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]}]", 890 | "schedules": "[['bath_timer','6','','','','1','01:00:00','',''],['bedroom_timer','6','','','','1','01:00:00','',''],['closet_timer','6','','','','1','10:00','',''],['fan_timer','6','','','','1','00:30:00','','']]", 891 | "actions": "{'bedroom_start':[{'delay':0,'actions':[{'device':153,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'bedroom_timer'},{'name':'intervalTime','value':''}]}]}],'closet_start':[{'delay':0,'actions':[{'device':153,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'closet_timer'},{'name':'intervalTime','value':''}]}]}],'bath_start':[{'delay':0,'actions':[{'device':153,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'bath_timer'},{'name':'intervalTime','value':''}]}]}],'fan_start':[{'delay':0,'actions':[{'device':153,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'fan_timer'},{'name':'intervalTime','value':''}]}]}],'fan_expire':[{'delay':0,'actions':[{'device':'218','service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'closet_expire':[{'delay':0,'actions':[{'device':66,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'bedroom_expire':[{'delay':0,'actions':[{'device':70,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'0'}]}]}],'bath_expire':[{'delay':0,'actions':[{'device':'219','service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]},{'device':67,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'bedroom_extend':[{'delay':0,'actions':[{'device':153,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'bedroom_timer'},{'name':'intervalTime','value':''}]}]}],'lua_OutputVariableNames':{}}" 892 | }, { 893 | "name": "JJ Bath Fan", 894 | "altid": "71", 895 | "id": 100, 896 | "category": 3, 897 | "subcategory": 3, 898 | "room": 17, 899 | "parent": 1, 900 | "status": "0" 901 | }, { 902 | "name": "JJ Bath PLEG", 903 | "altid": "", 904 | "id": 155, 905 | "category": 0, 906 | "subcategory": -1, 907 | "room": 17, 908 | "parent": 0, 909 | "armed": "1", 910 | "conditionsatisfied": "None", 911 | "conditionrepeat": "{}", 912 | "objectstatusmap": "{{name = 'vanity_timer', state = false, seq = 1501873905.3397, oseq = 1501877505.1005},{name = 'fan_timer', state = false, seq = 1501807518.2907, oseq = 1501809318.1004},{name = 'shower_timer', state = false, seq = 1501873935.3373, oseq = 1501877535.1016},{name = 'fan_start', state = false, seq = 1501807518.237, oseq = 1501809318.3336},{name = 'fan_expire', state = false, seq = 1501809318.1047, oseq = 1501809318.3365},{name = 'shower_start', state = false, seq = 1501873935.2469, oseq = 1501875405.2372},{name = 'vanity_start', state = false, seq = 1501873905.2768, oseq = 1501875375.2875},{name = 'shower_expire', state = false, seq = 1500945119.108, oseq = 1500945119.3583},{name = 'vanity_expire', state = false, seq = 1501711663.1098, oseq = 1501711663.3746},{name = 'fan_on', state = false, seq = 1501807518.236, oseq = 1501809318.3324},{name = 'light_vanity', state = false, seq = 1501873905.2722, oseq = 1501875375.2827},{name = 'light_shower', state = false, seq = 1501873935.2425, oseq = 1501875405.2327},}", 913 | "triggers": "[{'name':'fan_on','device':'100','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_shower','device':'230','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'op':'=','name':'Status','value':'1'}]},{'name':'light_vanity','device':'34','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]}]", 914 | "actions": "{'fan_start':[{'delay':0,'actions':[{'device':155,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'fan_timer'},{'name':'intervalTime','value':''}]}]}],'fan_expire':[{'delay':0,'actions':[{'device':100,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'shower_start':[{'delay':0,'actions':[{'device':155,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'shower_timer'},{'name':'intervalTime','value':''}]}]}],'vanity_start':[{'delay':0,'actions':[{'device':155,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'vanity_timer'},{'name':'intervalTime','value':''}]}]}],'shower_expire':[{'delay':0,'actions':[{'device':'230','service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'vanity_expire':[{'delay':0,'actions':[{'device':34,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'lua_OutputVariableNames':{}}", 915 | "schedules": "[['fan_timer','6','','',null,'1','30:00','',''],['shower_timer','6','','',null,'1','01:00:00','',''],['vanity_timer','6','','',null,'1','01:00:00','','']]" 916 | }, { 917 | "name": "JJ Bath Vanity", 918 | "altid": "146", 919 | "id": 34, 920 | "category": 3, 921 | "subcategory": 3, 922 | "room": 17, 923 | "parent": 1, 924 | "status": "0", 925 | "commFailure": "0" 926 | }, { 927 | "name": "JJ Shower Light", 928 | "altid": "150", 929 | "id": 230, 930 | "category": 3, 931 | "subcategory": 3, 932 | "room": 17, 933 | "parent": 1, 934 | "status": "0", 935 | "commFailure": "0" 936 | }, { 937 | "name": "JJ Vanity Accessory", 938 | "altid": "138", 939 | "id": 96, 940 | "category": 14, 941 | "subcategory": 0, 942 | "room": 17, 943 | "parent": 1, 944 | "commFailure": "0", 945 | "status": "0" 946 | }, { 947 | "name": "Kitchen Desk", 948 | "altid": "32", 949 | "id": 39, 950 | "category": 3, 951 | "subcategory": 0, 952 | "room": 9, 953 | "parent": 1, 954 | "status": "0" 955 | }, { 956 | "name": "Kitchen Pendants", 957 | "altid": "29", 958 | "id": 37, 959 | "category": 3, 960 | "subcategory": 0, 961 | "room": 9, 962 | "parent": 1, 963 | "status": "0" 964 | }, { 965 | "name": "Kitchen Recessed", 966 | "altid": "30", 967 | "id": 38, 968 | "category": 2, 969 | "subcategory": 0, 970 | "room": 9, 971 | "parent": 1, 972 | "status": "0", 973 | "level": "0" 974 | }, { 975 | "name": "Kitchen Sink Task", 976 | "altid": "34", 977 | "id": 41, 978 | "category": 2, 979 | "subcategory": 0, 980 | "room": 9, 981 | "parent": 1, 982 | "status": "0", 983 | "level": "0", 984 | "armed": "0", 985 | "armedtripped": "0" 986 | }, { 987 | "name": "Kitchen Undercabinet", 988 | "altid": "33", 989 | "id": 40, 990 | "category": 3, 991 | "subcategory": 0, 992 | "room": 9, 993 | "parent": 1, 994 | "status": "0" 995 | }, { 996 | "name": "Lamp Plug-in Dim", 997 | "altid": "130", 998 | "id": 49, 999 | "category": 2, 1000 | "subcategory": 0, 1001 | "room": 11, 1002 | "parent": 1, 1003 | "status": "0", 1004 | "level": "0" 1005 | }, { 1006 | "name": "Landscaping", 1007 | "altid": "7209aece-979d-4263-a6ab-92a3b812d96b", 1008 | "id": 500, 1009 | "category": 0, 1010 | "subcategory": -1, 1011 | "room": 0, 1012 | "parent": 232, 1013 | "enabled": "1", 1014 | "remaining": "0", 1015 | "watering": "0", 1016 | "laststart": "1501873004", 1017 | "runends": "1501877419", 1018 | "duration": "74" 1019 | }, { 1020 | "name": "Laundry Lights", 1021 | "altid": "93", 1022 | "id": 164, 1023 | "category": 3, 1024 | "subcategory": 0, 1025 | "room": 18, 1026 | "parent": 1, 1027 | "status": "0" 1028 | }, { 1029 | "name": "Lounge Chandelier", 1030 | "altid": "14", 1031 | "id": 15, 1032 | "category": 2, 1033 | "subcategory": 0, 1034 | "room": 3, 1035 | "parent": 1, 1036 | "status": "0", 1037 | "level": "0" 1038 | }, { 1039 | "name": "Lounge Door Sconces", 1040 | "altid": "16", 1041 | "id": 18, 1042 | "category": 2, 1043 | "subcategory": 0, 1044 | "room": 7, 1045 | "parent": 1, 1046 | "status": "0", 1047 | "level": "0" 1048 | }, { 1049 | "name": "Lounge Motion", 1050 | "altid": "89", 1051 | "id": 130, 1052 | "category": 4, 1053 | "subcategory": 3, 1054 | "room": 3, 1055 | "parent": 1, 1056 | "armed": "0", 1057 | "lasttrip": "1501875009", 1058 | "tripped": "0", 1059 | "armedtripped": "0", 1060 | "batterylevel": "100", 1061 | "commFailure": "0" 1062 | }, { 1063 | "name": "Lounge PLTS", 1064 | "altid": "", 1065 | "id": 139, 1066 | "category": 0, 1067 | "subcategory": -1, 1068 | "room": 3, 1069 | "parent": 0, 1070 | "iconstate": "0", 1071 | "status": "0", 1072 | "state": "0", 1073 | "armed": "1", 1074 | "interval": "7200", 1075 | "interval2": "1800", 1076 | "resetstate": "0", 1077 | "switchmode": "1", 1078 | "offcondition": "1501611656", 1079 | "conditionrepeat": "{'Restart':true}", 1080 | "objectstatusmap": "{{name = 'Arm', state = nil, seq = 0, oseq = 1462470715.7694},{name = 'Bypass', state = nil, seq = 0, oseq = 1462470715.7696},{name = 'Reset', state = nil, seq = 0, oseq = 1462470715.7697},{name = 'Trigger', state = false, seq = 1501599829.2418, oseq = 1501604451.7819},{name = 'Trigger2', state = nil, seq = 0, oseq = 1462470715.77},{name = 'Restart', state = false, seq = 1501875003.2146, oseq = 1501875009.9243},{name = 'Restart2', state = nil, seq = 0, oseq = 1462470715.7703},{name = 'On', state = nil, seq = 0, oseq = 1462470715.7705},{name = 'Off', state = nil, seq = 0, oseq = 1462470715.7707},{name = 'motion', state = false, seq = 1501875003.2098, oseq = 1501875009.9195},{name = 'light_bar_back', state = false, seq = 1501446711.6362, oseq = 1501447921.4598},{name = 'light_bar_sink', state = false, seq = 1501446711.7672, oseq = 1501447921.5902},{name = 'light_bar_top', state = false, seq = 1501446711.5063, oseq = 1501447921.31},{name = 'light_chandelier', state = false, seq = 1501446711.3763, oseq = 1501447921.1799},{name = 'light_far', state = false, seq = 1501599844.2377, oseq = 1501604451.778},{name = 'light_near', state = false, seq = 1501599829.2378, oseq = 1501604451.6675},{name = 'light_hall', state = false, seq = 1501446710.986, oseq = 1501447920.7698},{name = 'timer_on', state = '', seq = 1462473811.1153, oseq = 0},}", 1081 | "triggers": "[{'name':'motion','device':'130','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]},{'name':'light_bar_back','device':'20','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_bar_sink','device':'21','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_bar_top','device':'19','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_chandelier','device':'15','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_far','device':'14','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_near','device':'13','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_hall','device':'22','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]}]", 1082 | "properties": "[['timer_on','139','urn:rts-services-com:serviceId:ProgramLogicTS','On']]", 1083 | "actions": "{'Off':[{'delay':0,'actions':[{'device':139,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'10'}]}]}]}" 1084 | }, { 1085 | "name": "Lounge SC7", 1086 | "altid": "191", 1087 | "id": 16, 1088 | "category": 14, 1089 | "subcategory": 0, 1090 | "room": 3, 1091 | "parent": 1, 1092 | "commFailure": "0" 1093 | }, { 1094 | "name": "Main", 1095 | "altid": "therm.02AA01AC15130083", 1096 | "id": 142, 1097 | "category": 5, 1098 | "subcategory": -1, 1099 | "room": 8, 1100 | "parent": 136, 1101 | "fanmode": "Auto", 1102 | "fan": "On", 1103 | "hvacstate": "Cooling", 1104 | "mode": "CoolOn", 1105 | "temperature": "72", 1106 | "setpoint": "72", 1107 | "heat": "72", 1108 | "cool": "72", 1109 | "commands": "hvac_off,hvac_auto,hvac_state,heating_setpoint,cooling_setpoint,hvac_heat,hvac_cool,fan_label,fan_auto,fan_on", 1110 | "batterylevel": "96" 1111 | }, { 1112 | "name": "Main Humidity", 1113 | "altid": "humid.02AA01AC15130083", 1114 | "id": 145, 1115 | "category": 16, 1116 | "subcategory": -1, 1117 | "room": 8, 1118 | "parent": 136, 1119 | "humidity": "48" 1120 | }, { 1121 | "name": "Main Recessed", 1122 | "altid": "176", 1123 | "id": 33, 1124 | "category": 2, 1125 | "subcategory": 3, 1126 | "room": 6, 1127 | "parent": 1, 1128 | "status": "0", 1129 | "level": "0" 1130 | }, { 1131 | "name": "Master Bath PLEG", 1132 | "altid": "", 1133 | "id": 149, 1134 | "category": 0, 1135 | "subcategory": -1, 1136 | "room": 20, 1137 | "parent": 0, 1138 | "armed": "1", 1139 | "conditionsatisfied": "None", 1140 | "conditionrepeat": "{}", 1141 | "objectstatusmap": "{{name = 'fan_timer', state = false, seq = 1501793063.3266, oseq = 1501794863.1004},{name = 'light_timer', state = false, seq = 1501813343.3075, oseq = 1501816943.1004},{name = 'fan_start', state = false, seq = 1501793063.2351, oseq = 1501795645.3408},{name = 'fan_timer_expire', state = false, seq = 1501794863.1082, oseq = 1501795645.3431},{name = 'light_timer_expire', state = false, seq = 1501332219.1093, oseq = 1501332221.2111},{name = 'light_leds', state = false, seq = 1501255134.1006, oseq = 1501258723.3341},{name = 'light_main', state = false, seq = 1501682345.2382, oseq = 1501685252.1231},{name = 'light_makeup', state = false, seq = 1501809328.8623, oseq = 1501809766.4477},{name = 'light_shower', state = false, seq = 1501682555.2317, oseq = 1501685480.2425},{name = 'light_vanity_l', state = false, seq = 1501727231.2381, oseq = 1501728751.2411},{name = 'light_vanity_r', state = false, seq = 1501813343.2497, oseq = 1501816303.2438},{name = 'light_wc', state = false, seq = 1501791548.2814, oseq = 1501793078.2314},{name = 'fan_on', state = false, seq = 1501793063.2308, oseq = 1501795645.3365},{name = 'main_start', state = false, seq = 1501813343.2726, oseq = 1501816303.2535},{name = 'main_on', state = false, seq = 1501813343.2533, oseq = 1501816303.2475},}", 1142 | "triggers": "[{'name':'fan_on','device':'89','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_leds','device':'85','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_main','device':'82','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_makeup','device':'87','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_shower','device':'124','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_vanity_l','device':'88','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_vanity_r','device':'86','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_wc','device':'90','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]}]", 1143 | "schedules": "[['fan_timer','6','','','','1','30:00','',''],['light_timer','6','','',null,'1','60:00','','']]", 1144 | "actions": "{'fan_start':[{'delay':0,'actions':[{'device':149,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'fan_timer'},{'name':'intervalTime','value':''}]}]}],'fan_timer_expire':[{'delay':0,'actions':[{'device':89,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'light_timer_expire':[{'delay':0,'actions':[{'device':149,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'21'}]}]}],'main_on':[{'delay':0,'actions':[]}],'main_start':[{'delay':0,'actions':[{'device':149,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'light_timer'},{'name':'intervalTime','value':''}]}]}],'lua_OutputVariableNames':{}}" 1145 | }, { 1146 | "name": "Master Closet", 1147 | "altid": "215", 1148 | "id": 165, 1149 | "category": 3, 1150 | "subcategory": 0, 1151 | "room": 20, 1152 | "parent": 1, 1153 | "status": "1" 1154 | }, { 1155 | "name": "MBR Alcove Recept", 1156 | "altid": "198", 1157 | "id": 80, 1158 | "category": 3, 1159 | "subcategory": 0, 1160 | "room": 19, 1161 | "parent": 1, 1162 | "status": "0" 1163 | }, { 1164 | "name": "MBR Fan Light", 1165 | "altid": "57", 1166 | "id": 79, 1167 | "category": 2, 1168 | "subcategory": 0, 1169 | "room": 19, 1170 | "parent": 1, 1171 | "status": "0", 1172 | "level": "0", 1173 | "commFailure": "0" 1174 | }, { 1175 | "name": "MBR Fan Motor", 1176 | "altid": "56", 1177 | "id": 78, 1178 | "category": 3, 1179 | "subcategory": 0, 1180 | "room": 19, 1181 | "parent": 1, 1182 | "status": "1", 1183 | "commFailure": "0" 1184 | }, { 1185 | "name": "MBR SC4", 1186 | "altid": "85", 1187 | "id": 123, 1188 | "category": 14, 1189 | "subcategory": 0, 1190 | "room": 10, 1191 | "parent": 1, 1192 | "commFailure": "0" 1193 | }, { 1194 | "name": "Mstr Bath Chandelier", 1195 | "altid": "61", 1196 | "id": 84, 1197 | "category": 2, 1198 | "subcategory": 0, 1199 | "room": 20, 1200 | "parent": 1, 1201 | "status": "0", 1202 | "level": "0" 1203 | }, { 1204 | "name": "Mstr Bath LEDs", 1205 | "altid": "62", 1206 | "id": 85, 1207 | "category": 2, 1208 | "subcategory": 0, 1209 | "room": 20, 1210 | "parent": 1, 1211 | "status": "0", 1212 | "level": "0" 1213 | }, { 1214 | "name": "Mstr Bath Main", 1215 | "altid": "59", 1216 | "id": 82, 1217 | "category": 2, 1218 | "subcategory": 0, 1219 | "room": 20, 1220 | "parent": 1, 1221 | "status": "0", 1222 | "level": "0" 1223 | }, { 1224 | "name": "Mstr Bath Makeup", 1225 | "altid": "64", 1226 | "id": 87, 1227 | "category": 2, 1228 | "subcategory": 0, 1229 | "room": 20, 1230 | "parent": 1, 1231 | "status": "0", 1232 | "level": "0" 1233 | }, { 1234 | "name": "Mstr Bath Shower", 1235 | "altid": "86", 1236 | "id": 124, 1237 | "category": 2, 1238 | "subcategory": 0, 1239 | "room": 20, 1240 | "parent": 1, 1241 | "status": "0", 1242 | "level": "0" 1243 | }, { 1244 | "name": "Mstr Bath Vanity L", 1245 | "altid": "65", 1246 | "id": 88, 1247 | "category": 2, 1248 | "subcategory": 0, 1249 | "room": 20, 1250 | "parent": 1, 1251 | "status": "0", 1252 | "level": "0" 1253 | }, { 1254 | "name": "Mstr Bath Vanity R", 1255 | "altid": "63", 1256 | "id": 86, 1257 | "category": 2, 1258 | "subcategory": 0, 1259 | "room": 20, 1260 | "parent": 1, 1261 | "status": "0", 1262 | "level": "0" 1263 | }, { 1264 | "name": "Mstr WC Fan", 1265 | "altid": "66", 1266 | "id": 89, 1267 | "category": 3, 1268 | "subcategory": 0, 1269 | "room": 20, 1270 | "parent": 1, 1271 | "status": "0" 1272 | }, { 1273 | "name": "Mstr WC Light", 1274 | "altid": "67", 1275 | "id": 90, 1276 | "category": 2, 1277 | "subcategory": 0, 1278 | "room": 20, 1279 | "parent": 1, 1280 | "status": "0", 1281 | "level": "0" 1282 | }, { 1283 | "name": "Multisensor 6 in 1", 1284 | "altid": "111", 1285 | "id": 225, 1286 | "category": 4, 1287 | "subcategory": 3, 1288 | "room": 16, 1289 | "parent": 1, 1290 | "armedtripped": "0", 1291 | "lasttrip": "1501877885", 1292 | "tripped": "0", 1293 | "armed": "0", 1294 | "batterylevel": "100" 1295 | }, { 1296 | "name": "Near Recessed", 1297 | "altid": "177", 1298 | "id": 32, 1299 | "category": 2, 1300 | "subcategory": 3, 1301 | "room": 6, 1302 | "parent": 1, 1303 | "status": "0", 1304 | "level": "0" 1305 | }, { 1306 | "name": "Nest", 1307 | "altid": "", 1308 | "id": 136, 1309 | "category": 0, 1310 | "subcategory": -1, 1311 | "room": 23, 1312 | "parent": 0 1313 | }, { 1314 | "name": "Office Lamp ELR", 1315 | "altid": "154", 1316 | "id": 60, 1317 | "category": 2, 1318 | "subcategory": 0, 1319 | "room": 13, 1320 | "parent": 1, 1321 | "status": "0", 1322 | "level": "0" 1323 | }, { 1324 | "name": "Office Lamp PHR", 1325 | "altid": "155", 1326 | "id": 58, 1327 | "category": 2, 1328 | "subcategory": 2, 1329 | "room": 13, 1330 | "parent": 1, 1331 | "status": "0", 1332 | "level": "0" 1333 | }, { 1334 | "name": "Office Lights", 1335 | "altid": "207", 1336 | "id": 53, 1337 | "category": 2, 1338 | "subcategory": 0, 1339 | "room": 13, 1340 | "parent": 1, 1341 | "status": "1", 1342 | "level": "100" 1343 | }, { 1344 | "name": "Pathway", 1345 | "altid": "10", 1346 | "id": 11, 1347 | "category": 2, 1348 | "subcategory": 0, 1349 | "room": 2, 1350 | "parent": 1, 1351 | "status": "0", 1352 | "level": "0" 1353 | }, { 1354 | "name": "Pathway (3-way)", 1355 | "altid": "37", 1356 | "id": 46, 1357 | "category": 3, 1358 | "subcategory": 0, 1359 | "room": 11, 1360 | "parent": 1, 1361 | "status": "0", 1362 | "level": "0" 1363 | }, { 1364 | "name": "Patio Chandeliers", 1365 | "altid": "15", 1366 | "id": 17, 1367 | "category": 2, 1368 | "subcategory": 0, 1369 | "room": 7, 1370 | "parent": 1, 1371 | "status": "0", 1372 | "level": "0" 1373 | }, { 1374 | "name": "Play Room Closet", 1375 | "altid": "72", 1376 | "id": 101, 1377 | "category": 3, 1378 | "subcategory": 3, 1379 | "room": 22, 1380 | "parent": 1, 1381 | "status": "0" 1382 | }, { 1383 | "name": "Play Room Fan", 1384 | "altid": "73", 1385 | "id": 102, 1386 | "category": 3, 1387 | "subcategory": 3, 1388 | "room": 22, 1389 | "parent": 1, 1390 | "status": "1" 1391 | }, { 1392 | "name": "Play Room Light", 1393 | "altid": "70", 1394 | "id": 99, 1395 | "category": 2, 1396 | "subcategory": 3, 1397 | "room": 22, 1398 | "parent": 1, 1399 | "status": "0", 1400 | "level": "0" 1401 | }, { 1402 | "name": "Play Room Motion", 1403 | "altid": "74", 1404 | "id": 104, 1405 | "category": 4, 1406 | "subcategory": 3, 1407 | "room": 22, 1408 | "parent": 1, 1409 | "armedtripped": "0", 1410 | "lasttrip": "1481590822", 1411 | "tripped": "0", 1412 | "armed": "0", 1413 | "batterylevel": "100", 1414 | "status": "0" 1415 | }, { 1416 | "name": "Plug-in Dimmer 213", 1417 | "altid": "170", 1418 | "id": 50, 1419 | "category": 2, 1420 | "subcategory": 0, 1421 | "room": 12, 1422 | "parent": 1, 1423 | "status": "0", 1424 | "level": "0" 1425 | }, { 1426 | "name": "Porch Fan Light", 1427 | "altid": "45", 1428 | "id": 56, 1429 | "category": 3, 1430 | "subcategory": 0, 1431 | "room": 14, 1432 | "parent": 1, 1433 | "status": "0" 1434 | }, { 1435 | "name": "Porch Fan Motor", 1436 | "altid": "44", 1437 | "id": 55, 1438 | "category": 3, 1439 | "subcategory": 0, 1440 | "room": 14, 1441 | "parent": 1, 1442 | "status": "0" 1443 | }, { 1444 | "name": "PTC-CH", 1445 | "altid": "loc.9e24f620-f629-11e1-b488-123139272b1d", 1446 | "id": 141, 1447 | "category": 5, 1448 | "subcategory": -1, 1449 | "room": 23, 1450 | "parent": 136, 1451 | "status": "1" 1452 | }, { 1453 | "name": "Rachio Service", 1454 | "altid": "", 1455 | "id": 232, 1456 | "category": 0, 1457 | "subcategory": -1, 1458 | "room": 0, 1459 | "parent": 0, 1460 | "serviceCheck": "0", 1461 | "lastupdate": "1501877841", 1462 | "raindelay": "0", 1463 | "commFailure": "0", 1464 | "message": "Initializing..." 1465 | }, { 1466 | "name": "Rachio-C698DE", 1467 | "altid": "fd2cc39d-ff05-4c9e-bc98-9749e519046d", 1468 | "id": 491, 1469 | "category": 0, 1470 | "subcategory": -1, 1471 | "room": 0, 1472 | "parent": 232, 1473 | "status": "ONLINE", 1474 | "on": "1", 1475 | "paused": "0", 1476 | "raindelay": "1", 1477 | "message": "Enabled", 1478 | "remaining": "0", 1479 | "watering": "0", 1480 | "raindelaytime": "0" 1481 | }, { 1482 | "name": "Reading Left", 1483 | "altid": "174", 1484 | "id": 29, 1485 | "category": 2, 1486 | "subcategory": 3, 1487 | "room": 6, 1488 | "parent": 1, 1489 | "status": "0", 1490 | "level": "0" 1491 | }, { 1492 | "name": "Reading Right", 1493 | "altid": "26", 1494 | "id": 30, 1495 | "category": 2, 1496 | "subcategory": 3, 1497 | "room": 6, 1498 | "parent": 1, 1499 | "status": "0", 1500 | "level": "0" 1501 | }, { 1502 | "name": "Rear Spots", 1503 | "altid": "5", 1504 | "id": 6, 1505 | "category": 2, 1506 | "subcategory": 0, 1507 | "room": 2, 1508 | "parent": 1, 1509 | "status": "0", 1510 | "level": "0" 1511 | }, { 1512 | "name": "Recessed Far", 1513 | "altid": "13", 1514 | "id": 14, 1515 | "category": 2, 1516 | "subcategory": 0, 1517 | "room": 3, 1518 | "parent": 1, 1519 | "status": "0", 1520 | "level": "0" 1521 | }, { 1522 | "name": "Recessed Near", 1523 | "altid": "12", 1524 | "id": 13, 1525 | "category": 2, 1526 | "subcategory": 0, 1527 | "room": 3, 1528 | "parent": 1, 1529 | "status": "0", 1530 | "level": "0" 1531 | }, { 1532 | "name": "Ryan Bedroom Light", 1533 | "altid": "152", 1534 | "id": 95, 1535 | "category": 2, 1536 | "subcategory": 3, 1537 | "room": 21, 1538 | "parent": 1, 1539 | "status": "0", 1540 | "level": "0" 1541 | }, { 1542 | "name": "Ryan Bedroom PLEG", 1543 | "altid": "", 1544 | "id": 154, 1545 | "category": 0, 1546 | "subcategory": -1, 1547 | "room": 21, 1548 | "parent": 0, 1549 | "armed": "1", 1550 | "conditionsatisfied": "None", 1551 | "conditionrepeat": "{}", 1552 | "objectstatusmap": "{{name = 'bedroom_start', state = false, seq = 1501875421.0343, oseq = 1501876890.2409},{name = 'closet_start', state = false, seq = 1501682480.2432, oseq = 1501683380.5065},{name = 'bedroom_expire', state = false, seq = 1501862508.1027, oseq = 1501872085.3279},{name = 'closet_expire', state = true, seq = 1501683380.0268, oseq = 1501682480.2691},{name = 'light_bedroom', state = false, seq = 1501875421.0331, oseq = 1501876890.2399},{name = 'light_closet', state = false, seq = 1501682480.2417, oseq = 1501683380.505},{name = 'bedroom_timer', state = true, seq = 1501875421.1373, oseq = 1501862508.1005},{name = 'closet_timer', state = false, seq = 1501682480.2662, oseq = 1501683380.0238},}", 1553 | "triggers": "[{'name':'light_bedroom','device':'95','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_closet','device':'98','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]}]", 1554 | "schedules": "[['bedroom_timer','6','','','','1','01:00:00','',''],['closet_timer','6','','','','1','00:15:00','','']]", 1555 | "actions": "{'bedroom_start':[{'delay':0,'actions':[{'device':154,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'bedroom_timer'},{'name':'intervalTime','value':''}]}]}],'closet_start':[{'delay':0,'actions':[{'device':154,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'closet_timer'},{'name':'intervalTime','value':''}]}]}],'bedroom_expire':[{'delay':0,'actions':[{'device':95,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'0'}]}]}],'closet_expire':[{'delay':0,'actions':[{'device':98,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}],'extend_bedroom':[{'delay':0,'actions':[{'device':154,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'bedroom_timer'},{'name':'intervalTime','value':''}]}]}]}", 1556 | "properties": "[]" 1557 | }, { 1558 | "name": "Ryan Ceiling Fan", 1559 | "altid": "97", 1560 | "id": 205, 1561 | "category": 3, 1562 | "subcategory": 0, 1563 | "room": 21, 1564 | "parent": 1, 1565 | "status": "1" 1566 | }, { 1567 | "name": "Ryan Closet Light", 1568 | "altid": "69", 1569 | "id": 98, 1570 | "category": 3, 1571 | "subcategory": 0, 1572 | "room": 21, 1573 | "parent": 1, 1574 | "status": "0" 1575 | }, { 1576 | "name": "Ryan Headboard", 1577 | "altid": "95", 1578 | "id": 167, 1579 | "category": 3, 1580 | "subcategory": 0, 1581 | "room": 21, 1582 | "parent": 1, 1583 | "status": "1", 1584 | "commFailure": "0" 1585 | }, { 1586 | "name": "Sconces Left", 1587 | "altid": "8", 1588 | "id": 9, 1589 | "category": 2, 1590 | "subcategory": 0, 1591 | "room": 2, 1592 | "parent": 1, 1593 | "status": "0", 1594 | "level": "0" 1595 | }, { 1596 | "name": "Sconces Right", 1597 | "altid": "7", 1598 | "id": 8, 1599 | "category": 2, 1600 | "subcategory": 0, 1601 | "room": 2, 1602 | "parent": 1, 1603 | "status": "0", 1604 | "level": "0", 1605 | "commFailure": "0" 1606 | }, { 1607 | "name": "SiteSensor", 1608 | "altid": "", 1609 | "id": 231, 1610 | "category": 4, 1611 | "subcategory": -1, 1612 | "room": 0, 1613 | "parent": 0, 1614 | "interval": "1800", 1615 | "url": "http:\/\/localhost\/port_3480\/data_request?id=sdata&output_format=json", 1616 | "lastval": "Patrick H. Rigney", 1617 | "armedtripped": "1", 1618 | "lasttrip": "1499347971", 1619 | "armed": "1", 1620 | "tripped": "1" 1621 | }, { 1622 | "name": "South Side Security", 1623 | "altid": "43", 1624 | "id": 54, 1625 | "category": 3, 1626 | "subcategory": 0, 1627 | "room": 14, 1628 | "parent": 1, 1629 | "status": "0" 1630 | }, { 1631 | "name": "Stair Motion ZWN-BPC", 1632 | "altid": "88", 1633 | "id": 129, 1634 | "category": 4, 1635 | "subcategory": 3, 1636 | "room": 2, 1637 | "parent": 1, 1638 | "lasttrip": "1501875023", 1639 | "tripped": "0", 1640 | "armed": "0", 1641 | "armedtripped": "0", 1642 | "batterylevel": "82", 1643 | "commFailure": "0", 1644 | "status": "0", 1645 | "state": -1, 1646 | "comment": "" 1647 | }, { 1648 | "name": "Stair Multisensor", 1649 | "altid": "98", 1650 | "id": 206, 1651 | "category": 4, 1652 | "subcategory": 3, 1653 | "room": 2, 1654 | "parent": 1, 1655 | "armed": "0", 1656 | "lasttrip": "1501875028", 1657 | "tripped": "0", 1658 | "armedtripped": "0", 1659 | "batterylevel": "100", 1660 | "temperature": "76.7", 1661 | "humidity": "52", 1662 | "light": "71.1", 1663 | "status": "1" 1664 | }, { 1665 | "name": "Stairway (3-way)", 1666 | "altid": "11", 1667 | "id": 12, 1668 | "category": 2, 1669 | "subcategory": 0, 1670 | "room": 2, 1671 | "parent": 1, 1672 | "status": "0", 1673 | "level": "0" 1674 | }, { 1675 | "name": "Stairway PLTS", 1676 | "altid": "", 1677 | "id": 157, 1678 | "category": 0, 1679 | "subcategory": -1, 1680 | "room": 2, 1681 | "parent": 0, 1682 | "iconstate": "0", 1683 | "status": "0", 1684 | "state": "0", 1685 | "armed": "1", 1686 | "interval": "60", 1687 | "interval2": "10", 1688 | "resetstate": "0", 1689 | "switchmode": "1", 1690 | "offcondition": "1501875083", 1691 | "conditionrepeat": "{'Restart':true}", 1692 | "objectstatusmap": "{{name = 'Arm', state = nil, seq = 0, oseq = 1462556655.6561},{name = 'Bypass', state = nil, seq = 0, oseq = 1462556655.6563},{name = 'Reset', state = nil, seq = 0, oseq = 1462556655.6565},{name = 'Trigger', state = false, seq = 1501874935.8534, oseq = 1501875083.3336},{name = 'Trigger2', state = nil, seq = 0, oseq = 1462556655.6569},{name = 'Restart', state = false, seq = 1501875023.8352, oseq = 1501875083.3355},{name = 'Restart2', state = nil, seq = 0, oseq = 1462556655.6571},{name = 'On', state = nil, seq = 0, oseq = 1462556655.6573},{name = 'Off', state = nil, seq = 0, oseq = 1462556655.6574},{name = 'motion_aeon', state = false, seq = 1501875012.9199, oseq = 1501875028.91},{name = 'motion_zwnbpc', state = false, seq = 1501875006.5498, oseq = 1501875023.8298},{name = 'light_on', state = false, seq = 1501874936.0898, oseq = 1501875083.3299},}", 1693 | "triggers": "[{'name':'light_on','device':'12','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'motion_aeon','device':'206','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]},{'name':'motion_zwnbpc','device':'129','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]}]", 1694 | "schedules": "[]", 1695 | "actions": "{'On':[{'delay':0,'actions':[{'device':12,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'50'}]}]}],'Off':[{'delay':0,'actions':[{'device':12,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'0'}]}]}]}" 1696 | }, { 1697 | "name": "Sunset Control PLEG", 1698 | "altid": "", 1699 | "id": 138, 1700 | "category": 0, 1701 | "subcategory": -1, 1702 | "room": 23, 1703 | "parent": 0, 1704 | "armed": "1", 1705 | "conditionsatisfied": "None", 1706 | "conditionrepeat": "{}", 1707 | "objectstatusmap": "{{name = 'sunset', state = false, seq = 1501805152.1004, oseq = 1501813122.1005},{name = 'c1', state = false, seq = 1501805152.1199, oseq = 1501813122.1016},{name = 'c2', state = true, seq = 1501813122.1027, oseq = 1501805152.4427},}", 1708 | "schedules": "[['sunset','2','-00:30:00t','1,2,3,4,5,6,7','','2','22:15:00','1,2,3,4,5,6,7','00:45:00']]", 1709 | "actions": "{'c1':[{'delay':0,'actions':[{'device':138,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'6'}]}]}],'c2':[{'delay':0,'actions':[{'device':138,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'7'}]}]}]}" 1710 | }, { 1711 | "name": "Terr Bath Motion", 1712 | "altid": "90", 1713 | "id": 131, 1714 | "category": 4, 1715 | "subcategory": 3, 1716 | "room": 4, 1717 | "parent": 1, 1718 | "armed": "0", 1719 | "lasttrip": "1501859354", 1720 | "tripped": "0", 1721 | "armedtripped": "0", 1722 | "batterylevel": "100", 1723 | "status": "0" 1724 | }, { 1725 | "name": "Terr Office Motion", 1726 | "altid": "96", 1727 | "id": 169, 1728 | "category": 4, 1729 | "subcategory": 3, 1730 | "room": 6, 1731 | "parent": 1, 1732 | "armed": "0", 1733 | "lasttrip": "1501875005", 1734 | "tripped": "0", 1735 | "armedtripped": "0", 1736 | "batterylevel": "100", 1737 | "status": "1" 1738 | }, { 1739 | "name": "Terr Office PLTS", 1740 | "altid": "", 1741 | "id": 150, 1742 | "category": 0, 1743 | "subcategory": -1, 1744 | "room": 6, 1745 | "parent": 0, 1746 | "iconstate": "0", 1747 | "status": "0", 1748 | "state": "0", 1749 | "armed": "1", 1750 | "interval": "1800", 1751 | "interval2": "60", 1752 | "resetstate": "0", 1753 | "switchmode": "1", 1754 | "offcondition": "1501697917", 1755 | "conditionrepeat": "{'Restart':true}", 1756 | "objectstatusmap": "{{name = 'Arm', state = nil, seq = 0, oseq = 1462476480.9183},{name = 'Bypass', state = nil, seq = 0, oseq = 1462476480.9185},{name = 'Reset', state = nil, seq = 0, oseq = 1462476480.9186},{name = 'Trigger', state = false, seq = 1501694848.2425, oseq = 1501697613.3377},{name = 'Trigger2', state = nil, seq = 0, oseq = 1462476480.9189},{name = 'Restart', state = false, seq = 1501874995.5935, oseq = 1501875005.644},{name = 'Restart2', state = nil, seq = 0, oseq = 1462476480.9191},{name = 'On', state = nil, seq = 0, oseq = 1462476480.9193},{name = 'Off', state = nil, seq = 0, oseq = 1462476480.9194},{name = 'light_main', state = false, seq = 1501694848.2394, oseq = 1501696333.2461},{name = 'light_far', state = false, seq = 1501696118.3268, oseq = 1501697613.3345},{name = 'light_near', state = false, seq = 1501694863.2595, oseq = 1501696348.2405},{name = 'light_left', state = false, seq = 1500917375.2838, oseq = 1500924575.8239},{name = 'light_right', state = false, seq = 1501083148.2363, oseq = 1501087248.3519},{name = 'motion', state = false, seq = 1501874995.5899, oseq = 1501875005.6404},}", 1757 | "triggers": "[{'name':'light_far','device':'31','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_left','device':'29','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_main','device':'33','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_near','device':'32','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_right','device':'30','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'motion','device':'169','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]}]", 1758 | "actions": "{'Off':[{'delay':0,'actions':[{'device':150,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'12'}]}]}]}" 1759 | }, { 1760 | "name": "Terrace", 1761 | "altid": "therm.02AA01AC121508PP", 1762 | "id": 144, 1763 | "category": 5, 1764 | "subcategory": -1, 1765 | "room": 2, 1766 | "parent": 136, 1767 | "fanmode": "Auto", 1768 | "fan": "On", 1769 | "hvacstate": "FanOnly", 1770 | "mode": "CoolOn", 1771 | "temperature": "74", 1772 | "setpoint": "74", 1773 | "heat": "74", 1774 | "cool": "74", 1775 | "commands": "hvac_off,hvac_auto,hvac_state,heating_setpoint,cooling_setpoint,hvac_heat,hvac_cool,fan_label,fan_auto,fan_on", 1776 | "batterylevel": "100" 1777 | }, { 1778 | "name": "Terrace Bath Fan", 1779 | "altid": "31", 1780 | "id": 25, 1781 | "category": 3, 1782 | "subcategory": 1, 1783 | "room": 4, 1784 | "parent": 1, 1785 | "status": "0" 1786 | }, { 1787 | "name": "Terrace Bath PLEG", 1788 | "altid": "", 1789 | "id": 152, 1790 | "category": 0, 1791 | "subcategory": -1, 1792 | "room": 4, 1793 | "parent": 0, 1794 | "armed": "1", 1795 | "conditionsatisfied": "None", 1796 | "conditionrepeat": "{'extend':true}", 1797 | "objectstatusmap": "{{name = 'nighttime', state = false, seq = 1501815600.1005, oseq = 1501843994.1004},{name = 'fan_timer', state = false, seq = 1500602971.3543, oseq = 1500604771.0378},{name = 'light_timer', state = false, seq = 1501859349.3307, oseq = 1501859949.1007},{name = 'fan_start', state = false, seq = 1500602971.2408, oseq = 1500604771.3263},{name = 'light_on', state = false, seq = 1501859349.128, oseq = 1501859949.9169},{name = 'light_day_start', state = false, seq = 1501859348.7396, oseq = 1501859349.1329},{name = 'light_night_start', state = false, seq = 1501127394.6683, oseq = 1501127394.9339},{name = 'extend', state = false, seq = 1501859349.3018, oseq = 1501859949.9212},{name = 'fan_on', state = false, seq = 1500602971.2395, oseq = 1500604771.3251},{name = 'light_shower', state = false, seq = 1481127265.9656, oseq = 1481128085.7374},{name = 'light_task', state = false, seq = 1501859349.1256, oseq = 1501859949.674},{name = 'light_vanity', state = false, seq = 1501859349.2951, oseq = 1501859949.9145},{name = 'motion', state = false, seq = 1501859348.7354, oseq = 1501859354.0454},{name = 'light_timer_expire', state = false, seq = 1501859949.1096, oseq = 1501859949.9238},{name = 'fan_timer_expire', state = false, seq = 1500604771.0493, oseq = 1500604771.3364},}", 1798 | "triggers": "[{'name':'fan_on','device':'25','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_shower','device':'97','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_task','device':'23','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'light_vanity','device':'24','template':'1','service':'urn:upnp-org:serviceId:SwitchPower1','args':[{'name':'Status','op':'=','value':'1'}]},{'name':'motion','device':'131','template':'3','service':'urn:micasaverde-com:serviceId:SecuritySensor1','args':[{'name':'Tripped','op':'=','value':'1'}]}]", 1799 | "schedules": "[['fan_timer','6','','','','1','30:00','',''],['light_timer','6','','','','1','10:00','',''],['nighttime','2','23:00:00','1,2,3,4,5,6,7','','2','r','1,2,3,4,5,6,7','']]", 1800 | "actions": "{'fan_start':[{'delay':0,'actions':[{'device':152,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'fan_timer'},{'name':'intervalTime','value':''}]}]}],'light_on':[{'delay':0,'actions':[{'device':152,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'light_timer'},{'name':'intervalTime','value':''}]}]}],'light_day_start':[{'delay':0,'actions':[{'device':23,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'1'}]},{'device':24,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'100'}]}]}],'light_night_start':[{'delay':0,'actions':[{'device':24,'service':'urn:upnp-org:serviceId:Dimming1','action':'SetLoadLevelTarget','arguments':[{'name':'newLoadlevelTarget','value':'10'}]}]}],'extend':[{'delay':0,'actions':[{'device':152,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'StartTimer','arguments':[{'name':'timerName','value':'light_timer'},{'name':'intervalTime','value':''}]}]}],'light_timer_expire':[{'delay':0,'actions':[{'device':152,'service':'urn:rts-services-com:serviceId:ProgramLogicC','action':'RunScene','arguments':[{'name':'SceneNameOrNumber','value':'28'}]}]}],'fan_timer_expire':[{'delay':0,'actions':[{'device':25,'service':'urn:upnp-org:serviceId:SwitchPower1','action':'SetTarget','arguments':[{'name':'newTargetValue','value':'0'}]}]}]}" 1801 | }, { 1802 | "name": "Terrace Bath Sho", 1803 | "altid": "23", 1804 | "id": 97, 1805 | "category": 3, 1806 | "subcategory": 0, 1807 | "room": 4, 1808 | "parent": 1, 1809 | "status": "0", 1810 | "commFailure": "0" 1811 | }, { 1812 | "name": "Terrace Bath Task", 1813 | "altid": "21", 1814 | "id": 23, 1815 | "category": 3, 1816 | "subcategory": 0, 1817 | "room": 4, 1818 | "parent": 1, 1819 | "status": "0" 1820 | }, { 1821 | "name": "Terrace Bath Vanity", 1822 | "altid": "22", 1823 | "id": 24, 1824 | "category": 2, 1825 | "subcategory": 0, 1826 | "room": 4, 1827 | "parent": 1, 1828 | "status": "0", 1829 | "level": "0" 1830 | }, { 1831 | "name": "Terrace Humidity", 1832 | "altid": "humid.02AA01AC121508PP", 1833 | "id": 147, 1834 | "category": 16, 1835 | "subcategory": -1, 1836 | "room": 2, 1837 | "parent": 136, 1838 | "humidity": "54" 1839 | }, { 1840 | "name": "Theater SC4", 1841 | "altid": "3", 1842 | "id": 4, 1843 | "category": 14, 1844 | "subcategory": 0, 1845 | "room": 2, 1846 | "parent": 1, 1847 | "commFailure": "0" 1848 | }, { 1849 | "name": "Top Stair SC1", 1850 | "altid": "52", 1851 | "id": 72, 1852 | "category": 14, 1853 | "subcategory": 0, 1854 | "room": 10, 1855 | "parent": 1, 1856 | "commFailure": "0" 1857 | }, { 1858 | "name": "Upstairs", 1859 | "altid": "therm.02AA01AC1513005P", 1860 | "id": 143, 1861 | "category": 5, 1862 | "subcategory": -1, 1863 | "room": 19, 1864 | "parent": 136, 1865 | "fanmode": "Auto", 1866 | "fan": "On", 1867 | "hvacstate": "Cooling", 1868 | "mode": "CoolOn", 1869 | "temperature": "74", 1870 | "setpoint": "71", 1871 | "heat": "71", 1872 | "cool": "71", 1873 | "commands": "hvac_off,hvac_auto,hvac_state,heating_setpoint,cooling_setpoint,hvac_heat,hvac_cool,fan_label,fan_auto,fan_on", 1874 | "batterylevel": "100" 1875 | }, { 1876 | "name": "Upstairs Humidity", 1877 | "altid": "humid.02AA01AC1513005P", 1878 | "id": 146, 1879 | "category": 16, 1880 | "subcategory": -1, 1881 | "room": 19, 1882 | "parent": 136, 1883 | "humidity": "55" 1884 | }, { 1885 | "name": "Vacation Mode", 1886 | "altid": "", 1887 | "id": 156, 1888 | "category": 0, 1889 | "subcategory": -1, 1890 | "room": 23, 1891 | "parent": 0, 1892 | "text1": "VIRTUAL", 1893 | "text2": "SWITCH", 1894 | "status": "0" 1895 | }, { 1896 | "name": "Vegetables", 1897 | "altid": "142695af-a398-4def-8a6b-0f16232ce35b", 1898 | "id": 501, 1899 | "category": 0, 1900 | "subcategory": -1, 1901 | "room": 0, 1902 | "parent": 232, 1903 | "enabled": "1", 1904 | "laststart": "1501491618", 1905 | "runends": "1501492269", 1906 | "duration": "11", 1907 | "remaining": "0", 1908 | "watering": "0" 1909 | }, { 1910 | "name": "VRCS4-MRZ Switch", 1911 | "altid": "4", 1912 | "id": 5, 1913 | "category": 3, 1914 | "subcategory": 0, 1915 | "room": 2, 1916 | "parent": 1, 1917 | "status": "0", 1918 | "commFailure": "0" 1919 | }, { 1920 | "name": "Window Recessed", 1921 | "altid": "9", 1922 | "id": 10, 1923 | "category": 2, 1924 | "subcategory": 0, 1925 | "room": 2, 1926 | "parent": 1, 1927 | "status": "0", 1928 | "level": "0" 1929 | }, { 1930 | "name": "Z1 Front Shrubs", 1931 | "altid": "e1f74ecc-6938-4f69-8f28-3940d6b027ff", 1932 | "id": 494, 1933 | "category": 0, 1934 | "subcategory": -1, 1935 | "room": 0, 1936 | "parent": 232, 1937 | "enabled": "1", 1938 | "remaining": "0", 1939 | "watering": "0" 1940 | }, { 1941 | "name": "Z4 Entry Lawn", 1942 | "altid": "ff98d274-8a19-4ef3-82f4-e52d4837ca20", 1943 | "id": 497, 1944 | "category": 0, 1945 | "subcategory": -1, 1946 | "room": 0, 1947 | "parent": 232, 1948 | "enabled": "1", 1949 | "remaining": "0", 1950 | "watering": "0" 1951 | }, { 1952 | "name": "Z5 Front Lawn", 1953 | "altid": "571b1130-3d12-45fd-820f-cff0673548dc", 1954 | "id": 492, 1955 | "category": 0, 1956 | "subcategory": -1, 1957 | "room": 0, 1958 | "parent": 232, 1959 | "enabled": "1", 1960 | "remaining": "0", 1961 | "watering": "0" 1962 | }, { 1963 | "name": "Z6 Rear Lawn", 1964 | "altid": "6d75faef-30fd-4dbd-8288-71f579b6f198", 1965 | "id": 498, 1966 | "category": 0, 1967 | "subcategory": -1, 1968 | "room": 0, 1969 | "parent": 232, 1970 | "enabled": "1", 1971 | "remaining": "0", 1972 | "watering": "0" 1973 | }, { 1974 | "name": "Z7 Veg", 1975 | "altid": "51cd069c-3cc4-42f5-8003-e6572e67680b", 1976 | "id": 499, 1977 | "category": 0, 1978 | "subcategory": -1, 1979 | "room": 0, 1980 | "parent": 232, 1981 | "enabled": "1", 1982 | "remaining": "0", 1983 | "watering": "0" 1984 | }], 1985 | "categories": [{ 1986 | "name": "Dimmable Switch", 1987 | "id": 2 1988 | }, { 1989 | "name": "On\/Off Switch", 1990 | "id": 3 1991 | }, { 1992 | "name": "Sensor", 1993 | "id": 4 1994 | }, { 1995 | "name": "Thermostat", 1996 | "id": 5 1997 | }, { 1998 | "name": "Generic IO", 1999 | "id": 11 2000 | }, { 2001 | "name": "Scene Controller", 2002 | "id": 14 2003 | }, { 2004 | "name": "Humidity Sensor", 2005 | "id": 16 2006 | }, { 2007 | "name": "Temperature Sensor", 2008 | "id": 17 2009 | }, { 2010 | "name": "Light Sensor", 2011 | "id": 18 2012 | }, { 2013 | "name": "UV Sensor", 2014 | "id": 28 2015 | }], 2016 | "loadtime": 1501877902, 2017 | "dataversion": 877902194, 2018 | "state": 1, 2019 | "comment": "Ready." 2020 | } 2021 | --------------------------------------------------------------------------------