Example: Alert page in visualization with sound notification

Task

The aim of this example is to create alert page which will appear on top of regular visualization pages. Besides you can run mp3 alert sound in parallel to text notification. Alerts are taken from Alerts tab. Please note that you have to have set one of your floors as default page. This alert page will copy template from the default page where you have the menu to return to any other page. Fill the parameters in the top part of the script, like Alert_Trigger_Group_Address, URL_Audio_File etc.

Resident script

Add the following script. After run once, it will disable itself automatically.

Source code    
  1. -- ******** Alert page module version 1.1 created by Erwin van der Zwart ********* --
  2. -- ** This script perform automaticly all needed actions to support alert page *** --
  3. -- After running script once refresh browser and write alert for trigger the page --
  4. -- ******************************** SET PARAMETERS ******************************* --
  5.  
  6. --!!! Important !!! Set 1 pc/visu page to use as template to 'Show, make default', if no page is set to 'Show, make default' the script exits without creation of page !!
  7. --!!! Important !!! Works only on iOS after opening visu by "Add to homescreen" (audio can't be fired without touch event by Apple in normal mode)
  8.  
  9. --Give a (not already excisting) name for the plan thats holds the alert page module (if already exist then script exit the creation)
  10. Alert_Page_Name = 'Alerts'
  11.  
  12. -- Group address to (re)trigger the alert page and audio without new alert (for mandatory acknowledge by KNX)
  13. Alert_Trigger_Group_Address = '0/0/30' -- !! MUST BE A BYTE SCALE (5.001) OBJECT !! 0 = NOTHING, 1 = ACTIVATE, 2 = DEACTIVATE / MUTE, 3 = NEXT, 4 = PREVIOUS, 5 = PAGE CHANGE
  14.  
  15. -- Check for alerts without sending KNX command (if alert is added by script by: alert('alertmessage') to the alert list the action is fired automaticly)
  16. Check_Alerts_Automaticly = true
  17.  
  18. -- URL to use for downloading a audiofile to the controller
  19. URL_Audio_File = 'http://www.freesfx.co.uk/rx2/mp3s/1/814_1244852507.mp3'
  20.  
  21. -- Downloaded audio file format
  22. Audio_File_Format = 'mp3'
  23.  
  24. -- !IMPORTANT! internet must be available for the controller to download a audio file
  25. -- check DNS and default gateway or copy your own audio files to the img / backgrounds
  26. -- folder and name it 'alert' (format like Audio_File_Format)
  27.  
  28. -- Set interval (in seconds) for audio repeat playing file until mute is pressed
  29. Audio_Play_Interval_Time = 10 -- Adjust to length of playing file
  30.  
  31. -- Set interval (in seconds) for searching new alerts
  32. New_Alert_Polling_Interval_Time = 5 -- Adjust to long as possible to reduce browser load (check once each minute will result in max delay 1 minute)
  33.  
  34. -- Set title for HTML module (use '' for no title)
  35. Alert_Title = 'LM Alerts'
  36.  
  37. -- Set prefix for message info screen
  38. Alert_Message_Prefix = 'Message: '
  39. Alert_Time_Prefix = 'Time: '
  40. Alert_Scriptname_Prefix = 'Scriptname: '
  41.  
  42. -- Hide HTML buttons (to hide buttons when only KNX buttons are used)
  43. Hide_HTML_Buttons = false
  44.  
  45. -- Centralize mute / acknowledge when using HTML buttons (use when multiple screens must be muted@ once) !! IMPORTANT!! Group address to (re)trigger must be available (See parameters above)
  46. Central_Mute = true -- Not needed when using KNX buttons instead of HTML buttons
  47.  
  48. -- Set name for mute button
  49. Mute_Button_Text = 'Mute'
  50.  
  51. -- Set Width and Height of HTML frame on page (Must be smaller then size of default startpage)
  52. Alert_Frame_Width = 500
  53. Alert_Frame_Height = 400
  54.  
  55. -- ******************************** END PARAMETERS ****************************** --
  56. -- ********************* DON'T CHANGE ANYTHING UNDER THIS LINE ****************** --
  57.  
  58. -- Get default startpage
  59. query = 'SELECT id, building, layout, name, usermode_param, width, height FROM visfloors'
  60. for _, floor in ipairs(db:getall(query)) do
  61. if floor.usermode_param == "D" then
  62. default_startpage = floor.id
  63. alert_page_building = floor.building
  64. alert_page_layout = floor.layout
  65. alert_page_width = floor.width
  66. alert_page_height = floor.height
  67. end
  68. if floor.name == Alert_Page_Name then
  69. name_already_exists = true
  70. end
  71. end
  72.  
  73. -- Check if default page excists else exit script
  74. if default_startpage == nil then
  75. alert("Default page does not excists, exit alert page creation")
  76. -- Disable script
  77. script.disable(_SCRIPTNAME)
  78. --Exit script
  79. return
  80. end
  81.  
  82. -- Enable page adding
  83. Page_Adding = true
  84.  
  85. -- Check alert page already excists else exit script
  86. if name_already_exists == true then
  87. alert("Alert page already excist or name is already used, only HTML will be rewritten")
  88. -- Disable page adding
  89. Page_Adding = false
  90. end
  91.  
  92. -- Set sortorder to default sortorder value for counting
  93. highest_sortorder = 1
  94.  
  95. -- Get highest sortorder inside building part where default startpage is located
  96. query = 'SELECT sortorder FROM visfloors WHERE building == ' .. alert_page_building .. ' ORDER BY sortorder DESC'
  97. for _, floor in ipairs(db:getall(query)) do
  98. if highest_sortorder <= floor.sortorder then
  99. highest_sortorder = floor.sortorder + 1
  100. end
  101. end
  102.  
  103. if Page_Adding == true then
  104. -- Create alert page if alert page doesn't exist
  105. db:insert('visfloors', {building = alert_page_building, layout = alert_page_layout, sortorder = highest_sortorder, name = Alert_Page_Name, bgrepeat = 0, bgfixed = 0, adminonly = 0, width = alert_page_width, height = alert_page_height,})
  106. end
  107.  
  108. -- Get page number of new created alert page
  109. alert_page_number = db:getone('SELECT id FROM visfloors WHERE name=?', Alert_Page_Name)
  110.  
  111. -- Set interval (in seconds) for audio repeat playing file until mute is pressed to milliseconds
  112. Audio_Play_Interval_Time = Audio_Play_Interval_Time * 1000
  113. if Audio_Play_Interval_Time < 1000 then
  114. Audio_Play_Interval_Time = 1000
  115. end
  116.  
  117. -- Set interval (in seconds) for polling to milliseconds
  118. New_Alert_Polling_Interval_Time = New_Alert_Polling_Interval_Time * 1000
  119. if New_Alert_Polling_Interval_Time < 1000 then
  120. New_Alert_Polling_Interval_Time = 1000
  121. end
  122.  
  123. -- Create HTML content
  124. page = [[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  125. <html xmlns="http://www.w3.org/1999/xhtml">
  126. <head>
  127. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  128. <title>LM Alert Page</title>
  129. <style type="text/css">
  130. body {
  131. background-color: transparent;
  132. }
  133. </style>
  134. </head>
  135. <body>
  136. <div id="Wrapper" style="width:100%;">
  137. <div id="Title" style="height:20px; width:100%; resize:none; text-align: center; padding-top: 5px; font-family: Verdana, Geneva, sans-serif; font-weight: bold;">
  138. <label>]] .. Alert_Title .. [[</label>
  139. </div>
  140. <div id="Messagefield">
  141. <textarea id="alertmessage" name="Alerts" cols="1" rows="1" readonly="readonly" style="height:150px; overflow:hidden; width:100%; resize:none"></textarea>
  142. </div>
  143. <div id="Title" style="height:20px; width:100%; resize:none; text-align: center; padding-top: 5px; font-family: Verdana, Geneva, sans-serif;">
  144. <label id="alertnumber">0 / 0</label>
  145. </div>]]
  146. if Hide_HTML_Buttons == false then
  147. page = page .. [[
  148. <div id="Buttons" style="width:100%; margin-top:10px">
  149. <div style="position:relative; float:left;">
  150. <input id="previous" name="Previous" style="height:40px; margin-left:10px; width:180px; resize:none" onclick="previousalert()" type="button" value="<<" />
  151. </div>
  152. <div style="position:relative; float:right;">
  153. <input id="next" name="Next" style="height:40px; width:180px; margin-right:5px; resize:none" onclick="nextalert()" type="button" value=">" />
  154. </div>
  155. </div>
  156. <div id="Mute" style="text-align: center; margin-left:10px; margin-right:5px;">
  157. <input name="Mute" type="button" id="mute" style="height:40px; text-align: center; width:100%; margin-top:10px; resize:none" onclick="stopalert()" value="]] .. Mute_Button_Text .. [[" />
  158. </div>]]
  159. else
  160. page = page .. [[
  161. <div id="Buttons" style="width:100%; height:0px; margin-top:0px">
  162. <div style="position:relative; float:left;">
  163. <input id="previous" name="Previous" style="height:0px; visibility:hidden; margin-left:10px; width:180px; resize:none" onclick="previousalert()" type="button" value="<<" />
  164. </div>
  165. <div style="position:relative; float:right;">
  166. <input id="next" name="Next" style="height:0px; visibility:hidden; width:180px; margin-right:5px; resize:none" onclick="nextalert()" type="button" value=">" />
  167. </div>
  168. </div>
  169. <div id="Mute" style="text-align: center; margin-left:10px; margin-right:5px;">
  170. <input name="Mute" type="button" id="mute" style="height:0px; visibility:hidden; text-align: center; width:100%; margin-top:10px; resize:none" onclick="stopalert()" value="]] .. Mute_Button_Text .. [[" />
  171. </div>]]
  172. end
  173. page = page .. [[
  174. </div>
  175. <script type="text/javascript">
  176.  
  177. // ***** Make link to parent from iframe *****
  178. var p = window.parent, root, addr;
  179. var ip = location.host;
  180. var Message_Prefix = "]] .. Alert_Message_Prefix .. [[";
  181. var Time_Prefix = "]] .. Alert_Time_Prefix .. [[";
  182. var Scriptname_Prefix = "]] .. Alert_Scriptname_Prefix .. [[";
  183. var alert_number = 0;
  184. var page_number = 0;
  185. // name = planStore[ currentPlanId ].name
  186.  
  187. if (p && p.objectStore) {
  188.  
  189. function getdatafromcontrolleroninit() {
  190. p.$.ajax({
  191. url: p.Scada.reqUrl('alerts'),
  192. type: 'POST',
  193. data: { limit: 50 },
  194. dataType: 'json',
  195. success: function(result) {
  196. // console.log(result);
  197. if ( result.data.length > 0 ) {
  198. localStorage.setItem("result", JSON.stringify(result)); // Saves data to local browser storage
  199. localStorage.setItem("resultnumber", result.data.length); // Saves number of data items to local browser storage
  200. localStorage.setItem("resultnumberinit", result.data.length); // Saves number of data items to local browser storage
  201. localStorage.setItem("resultnumbertimeinit", result.data[0].alerttime); // Saves number of data items to local browser storage
  202. }
  203. }
  204. });
  205. }
  206.  
  207. // ** Get alerts on init
  208. getdatafromcontrolleroninit();
  209.  
  210. function getdatafromcontroller() {
  211. p.$.ajax({
  212. url: p.Scada.reqUrl('alerts'),
  213. type: 'POST',
  214. data: { limit: 50 },
  215. dataType: 'json',
  216. success: function(result) {
  217. //console.log(result);
  218. if ( result.data.length > 0 ) {
  219. localStorage.setItem("result", JSON.stringify(result)); // Saves data to local browser storage
  220. localStorage.setItem("resultnumber", result.data.length); // Saves number of data items to local browser storage
  221. localStorage.setItem("resultnumbertimenew", result.data[0].alerttime); // Saves number of data items to local browser storage
  222. }
  223. }
  224. });
  225. var storagetimeinit = localStorage.getItem("resultnumbertimeinit");
  226. var storagetimenew = localStorage.getItem("resultnumbertimenew");
  227. if (typeof storagetimeinit !== 'undefined' && typeof storagetimenew !== 'undefined') {
  228. if ( storagetimeinit !== storagetimenew ) {
  229. // alert('New alert found');
  230. localStorage.setItem("resultnumbertimeinit", storagetimenew); // Saves new init time to local browser storage
  231. alert_number = 0;
  232. // Check if function is already loaded before calling it
  233. if (typeof extractalertdata == 'function') {
  234. extractalertdata(alert_number);
  235. }
  236. document.getElementById("next").disabled = true;
  237. document.getElementById("previous").disabled = false;
  238. // Check if function is already loaded before calling it
  239. if (typeof playalert == 'function') {
  240. playalert();
  241. }
  242. }
  243. }
  244. }
  245.  
  246. function extractalertdata(alertnumber) {
  247. var storageresult = localStorage.getItem("result"); // Grab local saved stringyfied ajax call data and save it to a variable
  248. if (typeof storageresult !== 'undefined') { // Checks if var is loaded with data already
  249. result = JSON.parse(storageresult); // Tranfer stringyfied ajax call data back to object
  250. //console.log(result); // Logs ajax call data transferred from localStorage
  251. if (result.data.length >= 1) {
  252. var obj = result.data[alert_number];
  253. //console.log(obj);
  254. var alert_key;
  255. var alert_value;
  256. var alert_id;
  257. var alert_time;
  258. var alert_message;
  259. var alert_scriptname;
  260.  
  261. for ( var key in obj) {
  262. alert_key = key;
  263. alert_value = obj[key].toString();
  264.  
  265. if ( alert_key == 'id' ) {
  266. alert_id = alert_value;
  267. }
  268.  
  269. if ( alert_key == 'alerttime' ) {
  270. alert_time = alert_value;
  271.  
  272. function timeConverter(UNIX_timestamp){
  273. var a = new Date(UNIX_timestamp*1000);
  274. var months = ['Jan','Feb','Maa','Apr','Mei','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
  275. var year = a.getFullYear();
  276. var month = months[a.getMonth()];
  277. var date = a.getDate() < 10 ? '0' + a.getDate() : a.getDate();
  278. var hour = a.getHours() < 10 ? '0' + a.getHours() : a.getHours();
  279. var min = a.getMinutes() < 10 ? '0' + a.getMinutes() : a.getMinutes();
  280. var sec = a.getSeconds() < 10 ? '0' + a.getSeconds() : a.getSeconds();
  281. var time = date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec ;
  282. return time;
  283. }
  284. alert_time = timeConverter(alert_time);
  285. }
  286.  
  287. if ( alert_key == 'alert' ) {
  288. alert_message = alert_value;
  289. }
  290.  
  291. if ( alert_key == 'scriptname' ) {
  292. alert_scriptname = alert_value;
  293. }
  294.  
  295. document.getElementById('alertmessage').value = Message_Prefix + alert_message + "\n" + "\n" + Time_Prefix + alert_time + "\n" + "\n" + Scriptname_Prefix + alert_scriptname;
  296. document.getElementById('alertnumber').innerHTML = (result.data.length - alert_number) + " / " + result.data.length;
  297. }
  298. }
  299. }
  300. }]]
  301. if Check_Alerts_Automaticly == true then
  302. page = page ..[[
  303.  
  304. // ** Check for new alerts
  305. var getdata = window.setInterval("getdatafromcontroller()",]] .. New_Alert_Polling_Interval_Time .. [[);
  306. ]]
  307. end
  308. page = page .. [[
  309.  
  310. // Load last alert on init
  311. extractalertdata(alert_number)
  312.  
  313. // Disable next button because value = 0 (latest alert is loaded)
  314. document.getElementById("next").disabled = true;
  315.  
  316. function nextalert() {
  317. var storageresultnumber = localStorage.getItem("resultnumber"); // Grab local saved stringyfied ajax call data record number and save it to a variable
  318. if (typeof storageresultnumber !== 'undefined') { // Checks if var is loaded with data already
  319. alert_number = alert_number - 1
  320. if ( alert_number <= 0 ) {
  321. alert_number = 0
  322. // Disable next button because value = 0 (newest alert is loaded)
  323. document.getElementById("next").disabled = true;
  324. }
  325. if ( alert_number <= ( storageresultnumber -1 ) ) {
  326. document.getElementById("previous").disabled = false;
  327. }
  328. extractalertdata(alert_number)
  329. }
  330. }
  331.  
  332. function previousalert() {
  333. var storageresultnumber = localStorage.getItem("resultnumber"); // Grab local saved stringyfied ajax call data record number and save it to a variable
  334. if (typeof storageresultnumber !== 'undefined') { // Checks if var is loaded with data already
  335. alert_number = alert_number + 1
  336. if ( alert_number >= ( storageresultnumber - 1 ) ) {
  337. alert_number = ( storageresultnumber -1 );
  338. // Disable previous button because value = storageresultnumber (oldest alert is loaded)
  339. document.getElementById("previous").disabled = true;
  340. }
  341. if ( alert_number > 0 ) {
  342. document.getElementById("next").disabled = false;
  343. }
  344. extractalertdata(alert_number)
  345. }
  346. }
  347.  
  348. // Create snd for playing audio
  349. var snd = new Audio("/user/alert.]] .. Audio_File_Format .. [[");
  350. // snd.currentTime = 0;
  351.  
  352. // Create alertaudio for loop playing audio
  353. var alertaudio = ""
  354.  
  355. function playalert() {
  356. // snd.stop(); // ** to avoid already running audio @ once
  357. // Create loop
  358. if(alertaudio==""){
  359. alertaudio = window.setInterval("playalert_loop()",]] .. Audio_Play_Interval_Time .. [[);
  360. }
  361. // Start loop direct
  362. playalert_loop()
  363. }
  364.  
  365. function playalert_loop() {
  366. // Jump to alertpage (only if other page is showed)
  367. if ( p.currentPlanId != ]] .. alert_page_number .. [[ ) {
  368. p.showPlan(]] .. alert_page_number .. [[);
  369. // Load last alert
  370. alert_number = 0;
  371. extractalertdata(alert_number);
  372. document.getElementById("next").disabled = true;
  373. document.getElementById("previous").disabled = false;
  374. }
  375.  
  376. if (snd.paused || snd.ended || snd.currentTime == 0 ) {
  377. snd.play();
  378. } else {
  379. // if (snd.paused || snd.currentTime > 0 && !snd.ended) {
  380. // }
  381. }
  382. }]]
  383. if Central_Mute == true then
  384. page = page .. [[
  385.  
  386. function stopalert() {
  387. if(alertaudio!=""){
  388. window.clearInterval(alertaudio)
  389. alertaudio=""
  390. }
  391. if (!snd.paused || !snd.ended || snd.currentTime > 0 ) {
  392. snd.pause();
  393. snd.currentTime = 0;
  394. }
  395. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 2, 'text');
  396. }
  397. ]]
  398. else
  399. page = page .. [[
  400.  
  401. function stopalert() {
  402. if(alertaudio!=""){
  403. window.clearInterval(alertaudio)
  404. alertaudio=""
  405. }
  406. if (!snd.paused || !snd.ended || snd.currentTime > 0 ) {
  407. snd.pause();
  408. snd.currentTime = 0;
  409. }
  410. }
  411. ]]
  412. end
  413. page = page .. [[
  414. // Add event listener on KNX addres for retrigger alert function without new alert action
  415. addr = p.Scada.encodeGroupAddress(']] .. Alert_Trigger_Group_Address .. [[');
  416. p.objectStore.addListener(addr, function(obj, type) {
  417.  
  418. // to avoid (re)trigger on opening page */
  419. if (type == 'init') {
  420. return;
  421. }
  422.  
  423. // no actions on value 0
  424. if ( obj.value == 0) {
  425. // Nothing free value for reset of KNX datapoint
  426. }
  427.  
  428. // start alerts on value 1
  429. if ( obj.value == 1 ) {
  430. playalert();
  431. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 0, 'text');
  432. }
  433.  
  434. // stop alerts on value 2
  435. if ( obj.value == 2 ) {
  436. stopalert();
  437. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 0, 'text');
  438. }
  439.  
  440. // next alerts on value 3
  441. if ( obj.value == 3 ) {
  442. nextalert();
  443. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 0, 'text');
  444. }
  445.  
  446. // previous alerts on value 4
  447. if ( obj.value == 4 ) {
  448. previousalert();
  449. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 0, 'text');
  450. }
  451.  
  452. // open page alerts on value 5
  453. if ( obj.value == 5 ) {
  454. // Jump to alertpage (only if other page is showed)
  455. if ( p.currentPlanId != ]] .. alert_page_number .. [[ ) {
  456. p.showPlan(]] .. alert_page_number .. [[);
  457. // Load last alert
  458. alert_number = 0;
  459. extractalertdata(alert_number);
  460. document.getElementById("next").disabled = true;
  461. document.getElementById("previous").disabled = false;
  462. }
  463. p.setObjectValue({ address: ']] .. Alert_Trigger_Group_Address .. [[', rawdatatype: 5001 }, 0, 'text');
  464. }
  465.  
  466. });
  467. }
  468. </script>
  469. </body>
  470. </html>
  471. ]]
  472.  
  473. -- Create temp HTML file to write to controller
  474. io.writefile("/www/user/alert.html", page)
  475.  
  476. -- Download alert file to controler local
  477. os.execute('wget -q -U Mozilla -O /www/user/alert.' .. Audio_File_Format .. ' ' .. URL_Audio_File .. '')
  478.  
  479. -- Check if frame is smaller than page
  480. Alert_Frame_Width = math.min(Alert_Frame_Width, alert_page_width)
  481. Alert_Frame_Height = math.min(Alert_Frame_Height, alert_page_height)
  482.  
  483. -- Calculate center position
  484. htmlxposition = math.floor((alert_page_width / 2) + 0.5) - math.floor((Alert_Frame_Width / 2) + 0.5)
  485. htmlyposition = math.floor((alert_page_height / 2) + 0.5) - math.floor((Alert_Frame_Height / 2) + 0.5)
  486.  
  487. -- Check if HTML object already exist
  488. object_exists = false
  489. query = 'SELECT floor, type, name, params FROM visobjects'
  490. for _, visobject in ipairs(db:getall(query)) do
  491. if visobject.floor == alert_page_number and (visobject.type == 9 or visobject.type == "9") and visobject.name == "alert" then
  492. object_exists = true
  493. end
  494. end
  495.  
  496. if Page_Adding == true then
  497. -- Create html object on alertpage
  498. db:insert('visobjects', {floor = alert_page_number, type = 9, params = '{"source":"url","url":"/user/alert.html","width":' .. Alert_Frame_Width .. ',"height":' .. Alert_Frame_Height .. '}', locx = htmlxposition , locy = htmlyposition , name = "alert", notouch = 1, nobg = 1,})
  499. log ("Alert page is added")
  500. elseif object_exists == false then
  501. -- Create html object on excisiting alertpage
  502. db:insert('visobjects', {floor = alert_page_number, type = 9, params = '{"source":"url","url":"/user/alert.html","width":' .. Alert_Frame_Width .. ',"height":' .. Alert_Frame_Height .. '}', locx = htmlxposition , locy = htmlyposition , name = "alert", notouch = 1, nobg = 1,})
  503. log ("HTML is added to excisiting alert page")
  504. else
  505. log ("HTML on alert page is updated")
  506. end
  507.  
  508. -- Disable script when done automaticly
  509. script.disable(_SCRIPTNAME)

Created by Erwin van der Zwart from Schneider Electric