Project - Alerts Monitor

All EasyNet nodes offer facility to send the changed status of any local switches or sensors.
This Alerts Monitor offers a central display of incoming sensor alerts from remote EasyNet nodes - it is configurable for 1 to 24 (or more) channels. 

UsageTarget_name   ALERT   channel_number      (lights the RED corresponding channel indicator LED)
UsageTarget_name   CLEAR   channel_number      (optional GREEN OFF for bi-state sensors, momentary sensors extinguish after timeout)

To use, embed  'SendQ Target_name Alert channel_number'  into the relevant event trigger subroutines of your EasyNet sensor nodes.

The unit is designed to be stand-alone using neo-pixels as channel indicators, therefore the screen display is secondary and not essential.
The neo-pixels could either be in-line sticks or strips to give a similar looking result to the screen display (above).
Or neo-pixel strings could be strategically placed behind a pictorial representation of the sensor locations (perhaps a floor plan printout, or aerial view, etc) to 'map' the positions of the triggers.
Every channel can be assigned a name, eg: channel 1 might be 'Driveway', channel 2 might be 'Garage' etc.
If an EasyNet Voice Announcer was available it could also speak the Alert and Clear messages, eg: "Driveway Alert", "Garage Door Open" etc.

A default 'NoRepeatTimeout' prevents re-triggering annoyance for the specified duration, plus channels can be assigned individual timeouts.
So the 'Front Porch' might have a 3 minute delay to give sufficient time to attend, before re-notifying of the callers presence in case it was missed.
A mailbox sensor might notify of the original delivery, then just keep reminding of a delivery once every hour until emptied.
Bi-state switches etc which send both an On state and an Off state switch the channel display from Alert red to Clear green.
Momentary red Alerts from PIRs etc will automatically extinguish after their timeout delay, unless a Clear is received to turn them green for off.
Re-occuring Alerts re-triggered during the no-retrigger timeout period will show as orange alerts, not red - but anything can be changed to suit.
The blue led of the 4-channel monitor above is using the channel 1 indicator to display the status of a local switch (eg: monitor On/Off). 
The Setup button is left in the script in case it comes in useful (rename it to suit), but it doesn't actually do anything other than write a wlog entry.

You could create your own EasyNet instruction names to add into instructionslist$ with a corresponding subroutine branch that reads data$ to get the channel number for offering facility to remotely change timeouts of a particular channel, or even enable /disable the monitor or just a channel.

title$ = "EasyNet Alert Monitor v1.0, by Electroguard"
nodename$  = ""                       'Assign a unique node name of your choice (if you forget, it will be called "Node" + its node IP)
groupname$ = "Alerts\Monitor\Alarms"  'concatenated group names are searched for a partial match
localIP$   = WORD$(IP$,1)
netIP$     = WORD$(localIP$,1,".") + "." + WORD$(localIP$,2,".") + "." + WORD$(localIP$,3,".") + "."
nodeIP$    = WORD$(localIP$,4,".")
udpport    = 5001                       'change to suit your own preference, but don't forget to do the same for all nodes
if nodename$ = "" then nodename$ = "Node" + nodeIP$   '
instructionslist$ = ucase$("Reply Rename Load Save Restart Relay1ON Relay1OFF Relay1Toggle Alert Clear Enable Disable") 'local shared subdirs
filename$ = word$(BAS.FILENAME$,1,".") + ".ini" 'comment this and next line if you don't want to load/save settings to ini file
channels = 6
dim name$(channels + 1)
dim led(channels + 1)
dim lastalert(channels + 1)
dim norepeat(channels + 1)
norepeatdefault = 1 * 1000 * 20  'default no-retrigger delay (mins) to prevent repeat triggers during specified timeout
for c = 1 to channels
 name$(c) = ""
 lastalert(c) = 0
 norepeat(c) = norepeatdefault
 led(c) = 1
next c
'Assign individual names and no-repeat timeouts manually if wished
'name$(1) = " Door ": norepeat(1) = 3 *1000 *60   '(3 min)
'name$(3) = " Gate ": norepeat(3) = 1 *1000 *60   '(1 min)
'name$(7) = " Workshop ": norepeat(7) = 30 *1000 *60  '(30 mins)
neo = channels     'change to match the number of neopixels in use, set to 0 if none
neo.setup 13       'change to your desired neo-pixel driver pin
neo.strip 0, neo -1, 0,0,0  'set all neopixels off
neopercent = 10   'percentage brightness
R = cint(255/100 * neopercent): G = cint(255/100 * neopercent): B = cint(255/100 * neopercent)
gosub Load                               'reads nodename$ from file if available
RXmsg$ = ""                              'variable to hold incoming message
instruction$ = ""                          'variable to hold incoming instruction
data$ = ""                                   'variable to hold any incoming data after the instruction
retryq$ = ""                                 'variable to hold all unexpired messages still waiting to be acknowledged
qdelimiter$ = "|"                          'separates messages in the retryq
time2live = 60                             'sent-message unacknowledged lifetime in seconds
ID$ = ""                                       'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag
ledpin = 13: pin.mode ledpin, output: ledoff = 1: pin(ledpin) = ledoff
relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = 0     'using active high qpio12 for relay1
button1pin = 0: pin.mode button1pin, input, pullup                           'using active low gpio0 button
interrupt button1pin, button1
sensor1pin = 14: pin.mode sensor1pin, input, pullup
interrupt sensor1pin, triggered
timer0 1000, heartbeat                  '
timer1 1000, Retry                      'periodic timer to keep resending unACKed msgs until they expire
onudp udpRX
wlog "OK"
onhtmlreload screen
gosub screen

a$ = "<div id='alerts'  style='display: table; margin-right: auto; margin-left: auto;'>"
a$ = a$ + "<BR><BR><table id='monitor' border='1'><tr>"
a$ = a$ + "<td style='color:dimgray;'>" + "Alerts" + "</td>"
for c = 1 to channels
 a$ = a$ + "<td>" + "<svg height='20' width='30'><circle cx='15' cy='10' r='9' stroke='black' fill='darkgray' id='led" + str$(c) + "'/></svg>"  + "</td>"
next c
a$ = a$ + "</tr>"
a$ = a$ + "<td style='color:dimgray;'>" + "channel" + "</td>"
'a$ = a$ + "<td>" + button$("Setup", setup, "but1") + "</td>"
for c = 1 to channels
 if name$(c) <> "" then a$ = a$ + "<td>" + name$(c)  + "</td>" else a$ = a$ + "<td>" + str$(c) + "</td>"
next c
a$ = a$ + "</tr></table>"
a$ = a$ + cssid$("monitor", "font: 15px arial, sans-serif;padding:10;background-color: lightgrey; color:blue; text-align:center;")
a$ = a$ + "</div>"
html a$
a$ = ""

wlog "Setup"

'wlog str$(ramfree)
for c = 1 to channels
 if lastalert(c) > 0 then
  if millis - norepeat(c) > lastalert(c) then
   lastalert(c) = 0
   'comment the next 2 lines if you prefer not to extinguish after the timeouts
   TRACE "csshtml" + CSSID$("led" + str$(c), "fill: darkgray;")
   neo.pixel c -1, 0,0,0
next c

RXmsg$ =$
if ucase$(word$(RXmsg$,1)) = "ACK" then
 gosub ACK   'echoed reply from successfully received message, original msg can be removed from queue
 target$ = ucase$(word$(RXmsg$,1))          'Target may be NodeName or GroupName or "ALL" or localIP address
 if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then
  instruction$ = trim$(ucase$(word$(RXmsg$,2)))  'Instruction is second word of message
  data$ = "": getdata data$,RXmsg$," ",2      'extract any data that follows the instruction
  if word.find(ucase$(instructionslist$),instruction$) > 0 then
   if (ucase$(instruction$) <> "ACK") and (instr(ucase$(data$),"ID=") > 0) then
    udp.reply "ACK " + RXmsg$                      'ACKnowledge the incoming msg
   gosub instruction$                                       'branch to action the corresponding instruction subroutine
  endif  'word.find
 endif   '(target$=localIP$)
endif 'ACK

msg$ = "": getdata msg$, RXmsg$, " ", 1
wlog "Ack recvd for " + msg$
pos = word.find(retryq$,msg$,qdelimiter$)
if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$)

if word.count(retryq$, qdelimiter$) > 0 then
 if retryq$ <> "" then wlog "queue=" + retryq$
 msg$ = word$(retryq$,1,qdelimiter$)               'grab first unACKed  msg in the queue
 retryq$ = word.delete$(retryq$,1,qdelimiter$)  'chop msg off front of queue
 expire$ = ""
 WordParse expire$, msg$, "ID=", " "                'parse out ID= expire time
 if msg$ <> "" then                                             'compare expire time to current unix time
  if dateunix(date$) + timeunix(time$) > val(expire$) then
   Send "LOG ERROR: Node " + Nodename$ + " FAILED SEND - " + msg$ + " not ACKnowledged"
   retryq$ = retryq$ + msg$ + qdelimiter$
   udp.write netip$ + "255", udpport, msg$
   wlog "retry " + msg$

if data$ <> "" then nodename$ = data$

LOAD: 'Load settings from file, by default is only looking for nodename, add your own saved parameters to look for if wished
a$ = ""
if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$)
if WORD.GETPARAM$(a$,"nodename$") <> "" then nodename$ = WORD.GETPARAM$(a$,"nodename$")

SAVE:  'Save settings to file, by default will only save nodename, add your own parameters to save if wished
a$ = ""
if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$)
WORD.SETPARAM  a$, "nodename$", nodename$
FILE.SAVE filename$, a$

reboot 'causes a device restart

sub SendQ(sendmsg$)
 sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1)
 retryq$ = retryq$ + sendmsg$ + qdelimiter$
 udp.write netip$ + "255", udpport, sendmsg$
end sub

sub Send(sendmsg$)
 udp.write netip$ + "255", udpport, sendmsg$
end sub

sub GetData(ret$, v$, sep$, pos)  'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb)
 local i, p, q
 p = 1
 for i = 1  to pos
  p = instr(p + 1, v$, sep$)
  if p > 0 then p = p + len(sep$)
 next i
 if p = 0 then
  ret$ = ""
  q = instr(p+1, v$, sep$)
  if q = 0 then q = 999
  ret$ = mid$(v$, p)
 end if
end sub

sub WordParse(ret$, full$, search$, sep$)  'extracts value from option=value (thanks cicciocb)
 local p, b$
 p = instr(full$, search$)
 if p <> 0 then
  b$ = mid$(full$, p + len(search$))
  ret$ = word$(b$, 1, sep$)
  ret$ = ""
 end if
end sub

udp.reply "Reply from " + Nodename$ + " Ramfree=" + str$(ramfree) + " Flashfree=" + str$(flashfree)

pin(relay1pin) = 1

pin(relay1pin) = 0

if pin(relay1pin) = 1 then pin(relay1pin) = 0 else pin(relay1pin) = 1

data$ = str$(2)  'set to required channel
if pin(button1pin) = 0 then
 gosub Alert
 gosub Clear    'comment out this line if you wish the button to act as momentary instead of bi-state

if pin(sensor1pin) = 1 then      'active high trigger
 ' sendq "All relay1ON"
 pin(ledpin) = 1 - ledoff
 pause 5000
 ' sendq "ALL Relay1Off"
 pin(ledpin) = ledoff
zone = val(data$)
'wlog "Alert:" + str$(zone)
if millis - norepeat(zone) > lastalert(zone) then
 TRACE "csshtml" + CSSID$("led" + str$(zone), "fill: red;")
 neo.pixel zone -1, R,0,0
 lastalert(zone) = millis
 TRACE "csshtml" + CSSID$("led" + str$(zone), "fill: orange;")
 neo.pixel zone -1, R,G/4,0
 lastalert(zone) = millis
zone = val(data$)
'wlog "Clear:" + str$(zone)
TRACE "csshtml" + CSSID$("led" + str$(zone), "fill: green;")
neo.pixel zone -1, 0,G,0
lastalert(zone) = 0
END   '-------------------- End ---------------------

Note to self: incorporate watchdog timer facility into Alerts Monitor to flag up any non communicating sensor nodes.