<?xml version="1.0" encoding="UTF-8"?>
<templates version="1.30.5">
<template prio="30" name="@string/speed_dial_title" label="@param-@string/uri" icon="@drawable/invite">
<keyConfiguration>
<lua>
<code><![CDATA[local gUserpath = "/identities/identity["..pIdentity.."]/"
local gSubscriptionIsTrying = false
local gSubscriptionIsWorking = false
local gPresenceIsTrying = false
local gPresenceIsWorking = false
local gUser = "" -- username of associated identity
local gDomain = "" -- domain (registrar) of associated identity
local gPickupCode = nil -- pickup-code of associated identity
local gDoPickupWithReplaces = false -- whether or not to use a replaces header (instead of pickup-code) to pick a call
local gRemoteUri = nil -- sipUri-object of remote
local gRemoteUser = nil -- user-part of remote uri
local gLabel = nil -- label set for the key (when != gRemoteUser -> show gRemoteUser in 2nd(info)-line)
local gOngoingSubscription = nil
local gOngoingPresenceSubscr = nil
local gWithSubscription = pSubscriptionEnabled=="true"
local gWithPresence = pSubscribePresence=="true"
local gWithPickup = pDoPickup=="true" and gWithSubscription
local gWithMissed = pBlinkOnMissed=="true"
local gWithAutoAnswer = pRequestAutoAnswer=="true"
local gHaveMissedCall = false
local gHideInMixedPickupScreen = pHideInMixedPickupScreen=="true"
local gShowPickupScreenOnPress = pShowPickupScreenOnPress=="true"
local gPickupConnected = pPickupConnected=="true"
local gShowingPickupList = false -- true while phone displays all pickable calls for this key. During this time we update the list whenever something changes
-- last list of pickable calls reported to pickup-screen. Use it to check new list for changes so sending same list again can be avoided.
local gLastReportedPickupInfos = nil
-- remember what we're signaling to the user, this will influence what is expected when key is pressed
-- one of:
-- - errors-state: error, trying
-- - subscription-event: idle, pickable, busy
-- - local call: calling, active, holding, ringing
-- - missed call: missed
-- - nothing going on: idle
local gLastReportedState = nil
local gPrevReportedState = nil
-- presence-state is one of: dnd, fwd, away, busy, available, unknown, offline
local gPresenceState = nil
-- presence-state-string is either the translated version of the current state or in case of unknown-state: contains note from presence-subscription. This String is shown in 2nd key-line whenever presence-infos are shown
local gPresenceStateString = nil
local gLocalCallId = nil -- wenn we have local call with gRemoteUser -> store its call-id here (e.g. TC@4)
local gLocalCallState = nil -- wenn we have local call with gRemoteUser -> store its state here (calling, active, holding or ringing)
local gFctName = "Blf" -- name for this key in logs (uri and identity will be added during initialization)
-- keep infos of all monitored calls on our remote, only used when gWithSubscription==true
-- contains these entries:
-- - "entity" : attribute from dialog-info-xml
-- - "version" : attribute from dialog-info-xml
-- - "metaState" : one of free/pickable/busy -> combined meta-state accumulated from all dialogs (monitored calls)
-- - "activeDialogs" : array of all dialog-ids of the all dialogs responsible for signaled meta-state.
-- - "dialogs" : map by dialog-id of all dialogs (monitored calls) (contains the received xml)
-- - "metaInfos" : map by dialog-id of meta-data for all reported dialogs
-- -> "name" : name that identifies party to pick
-- -> "state" : terminated / ringing / calling / connected
-- -> "stateTime" : when dialog first entered the current state (time.currentTimeMillis())
-- -> "replaces" : value of replaces-header needed to pick the guy
local gDialogInfo = nil
local function isValid() -- reports if key has all it's params set to valid values.
if pIdentity == "" or pIdentity == nil then
pIdentity = "1" -- old wui's allowed to set no identity, let's fix this for the user
end
if pRemote == "" or pRemote == nil then
return false
end
return true
end
local function getKeySettingsPath()
if key and key.INDEX and key.MODULE then
return "keys/"..key.MODULE.."/key[@keyNumber=\""..key.INDEX.."\"]"
end
return nil
end
-- Create value for replaces-header for provided dialog
local function getReplacesInfo(dlgXml)
local callId = dlgXml["call-id"]
if not callId or string.len(callId) == 0 then
debug.log(gFctName..": cannot find call-id for replaces-header", "e")
return nil
end
return sipTools.mkReplaces{callId=callId, to=dlgXml["remote-tag"], from=dlgXml["local-tag"]}
end
-- Create value for replaces-header for first pickable dialog
local function getFirstReplacesInfo()
if #gDialogInfo.activeDialogs == 0 or not gDialogInfo.metaState == "pickable" then
debug.log(gFctName..": cannot find pickable call for replaces", "e")
return nil
end
return getReplacesInfo(gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]])
end
-- analyze provided dialog and find a text that describes who our monitored remote is talking to
local function getRemoteName(dlgXml)
local remote = dlgXml:find("remote")
if not remote then
debug.log(gFctName..": missing <remote> within <dialog-info>, cannot name remote party", "w")
return ""
end
local dispName = remote:find("identity", "display")
if dispName and string.len(dispName.display) > 0 then return dispName.display end
local remote_usr = remote:find("identity")
if not remote_usr then remote_usr = remote:find("target") end
if not remote_usr or not remote_usr[1] then
debug.log(gFctName..": missing remote-uri within <dialog-info>, cannot name remote party", "w")
return ""
end
if string.find(string.lower(remote_usr[1]), "anonymous") then
return "@string/call_anonyme"
end
local remote_uri = sipTools.parseUri{sipUri=remote_usr[1]}
if not remote_uri or not remote_uri:getUiNumber() then
return remote_usr[1]
end
return remote_uri:getUiNumber()
end
-- analyze existing dialogs and find a text that describes who our monitored remote is talking to
local function getBlfRemoteText()
if #gDialogInfo.activeDialogs == 0 then
return nil
elseif #gDialogInfo.activeDialogs > 1 and not gDoPickupWithReplaces then
-- wouldn't know which one would be picked
return "@string/multiple_calls"
end
return getRemoteName(gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]])
end
local function updateKey()
local desiredInfo = nil
local desiredIcon = nil
local prevReportedState = gLastReportedState
-- default to showing tel-nr in 2nd line (when it's not already shown in 1st line)
if gLabel and gLabel ~= gRemoteUser and gLabel ~= "" then
desiredInfo = gRemoteUser
end
if not isValid() then
gLastReportedState = "error"
key:setLed("orange", false)
desiredInfo = "@string/setup_failed"
desiredIcon = "@drawable/block"
else
gLastReportedState = "idle"
if gLocalCallState == "calling" then
gLastReportedState = "calling"
key:setLed("red", true)
desiredInfo = "@string/callstate_calling"
elseif gLocalCallState == "ringing" then
gLastReportedState = "ringing"
key:setLed("red", true)
desiredInfo = "@string/callstate_ringing"
elseif gLocalCallState == "holding" then
gLastReportedState = "holding"
key:setLed("red", true)
desiredInfo = "@string/callstate_holding"
elseif gLocalCallState == "active" then
gLastReportedState = "active"
key:setLed("red", false)
desiredInfo = "@string/callstate_active"
elseif gWithSubscription then
if gSubscriptionIsWorking then
if gDialogInfo.metaState == "busy" then
gLastReportedState = "busy"
key:setLed("red", false)
desiredInfo = getBlfRemoteText()
elseif gDialogInfo.metaState == "pickable" then
gLastReportedState = "pickable"
key:setLed("red", true)
desiredIcon = "@drawable/pick_up"
desiredInfo = getBlfRemoteText()
else
gLastReportedState = "idle"
key:setLed("off")
end
elseif gSubscriptionIsTrying then
gLastReportedState = "trying"
key:setLed("orange", true)
desiredInfo = "@string/subscription_trying"
else
gLastReportedState = "error"
key:setLed("orange", false)
desiredInfo = "@string/subscription_failed"
end
else
key:setLed("off")
end
if gLastReportedState == "idle" and gHaveMissedCall then
gLastReportedState = "missed"
key:setLed("green", true)
desiredInfo = "@string/missed_call"
end
if gLastReportedState == "idle" and gWithPresence then
if gPresenceIsTrying then
gLastReportedState = "presence_trying"
key:setLed("orange", true)
desiredInfo = "@string/subscription_trying"
elseif not gPresenceIsWorking then
gLastReportedState = "presence_error"
key:setLed("orange", false)
desiredInfo = "@string/presence_failed"
else
gLastReportedState = "presence_"..gPresenceState
desiredInfo = gPresenceStateString
if gPresenceState == "available" then
key:setLed("green", false)
elseif gPresenceState == "dnd" or gPresenceState == "busy" then
key:setLed("red", false)
else
key:setLed("orange", false)
end
end
end
end
if not (prevReportedState == gLastReportedState) then
gPrevReportedState = prevReportedState
end
key:setInfo(desiredInfo)
key:setIcon(desiredIcon)
end
function onDbResult(entries)
gHaveMissedCall = false
for i, queryParam in ipairs(entries) do
local entryUser = sipTools.parseUri{sipUri=queryParam.number}:getUser()
if entryUser == gRemoteUser then
debug.log(gFctName..": found missed call from "..tostring(queryParam.number), "d")
gHaveMissedCall = true
break
end
end
updateKey()
end
function onCallEvent(event, call1, call2)
if event == nil or call1 == nil then
debug.log(gFctName..": invalid parameters at callListChanged("..tostring(event).." ,"..tostring(call1)..")", "e")
return
end
local callId = call1:getId()
local state = call1:getState()
local number, name = call1:getRemote()
debug.log("received "..tostring(event).." for call to "..tostring(number)..", state="..tostring(state)..", callId="..tostring(callId), "v")
local remoteUri = sipTools.parseUri{sipUri=number}
local remoteUser = remoteUri:getUser()
if remoteUser ~= gRemoteUser then
if callId == gLocalCallId then
debug.log("call-partner of local "..callId.." changed, call no longer belongs to "..gFctName, "d")
gLocalCallId = nil
gLocalCallState = nil
updateKey()
end
return
end
if state == "calling" or state == "ringing" or state == "active" or state == "holding" then
if gLocalCallId ~= callId then
debug.log(gFctName.." found new local call "..callId.." in state "..state, "d")
elseif gLocalCallState ~= state then
debug.log(gFctName.." "..callId.." changed state to "..state, "d")
end
gLocalCallId = callId
gLocalCallState = state
else
debug.log(gFctName.." "..callId.." ended", "d")
gLocalCallId = nil
gLocalCallState = nil
end
updateKey()
end
-- collect list of calls to present to phome
function collectPickupList()
local pickupInfos = {}
if not gDialogInfo then
return pickupInfos
end
for _, dlgId in ipairs(gDialogInfo.activeDialogs) do
local dlgXml = gDialogInfo.dialogs[dlgId]
local newPickInfo = {}
newPickInfo["other-name"] = key.TITLE
if gDialogInfo.metaInfos[dlgId] then
if (gDialogInfo.metaInfos[dlgId].state ~= "ringing") then
if (gPickupConnected and gDialogInfo.metaInfos[dlgId].state == "connected") then
-- thats fine, we also report connected
else
goto continueCollectPickupList
end
end
newPickInfo.state = gDialogInfo.metaInfos[dlgId].state
newPickInfo.stateTime = gDialogInfo.metaInfos[dlgId].stateTime
else
goto continueCollectPickupList
end
newPickInfo["pickup-name"] = getRemoteName(dlgXml)
local replHdr, replVal = getReplacesInfo(dlgXml)
newPickInfo.replaces = replVal
pickupInfos[dlgId] = newPickInfo
::continueCollectPickupList::
end
return pickupInfos
end
-- count the keys of a dictionary
function getDictSize(dictionary)
if dictionary == nil then
return 0
end
local result = 0
for _, _ in pairs(dictionary) do
result = result + 1
end
return result
end
-- check 2 pickup-lists for changes
function pickupListChanged(oldList, newList)
local oldSize = getDictSize(oldList)
local newSize = getDictSize(newList)
if oldSize == 0 then
return newSize > 0
end
if newList == nil or newSize ~= oldSize then
return true
end
for dlgId, infos in pairs(newList) do
if not oldList[dlgId] or
oldList[dlgId].state ~= newList[dlgId].state or
oldList[dlgId].replaces ~= newList[dlgId].replaces or
oldList[dlgId]["pickup-name"] ~= newList[dlgId]["pickup-name"] then
return true
end
end
return false
end
local function createDialogInfoStruct(dlgVers, dlgEntity)
local result = {}
result.entity = dlgEntity
result.version = dlgVers
result.metaState = "free"
result.activeDialogs = {}
result.dialogs = {}
return result
end
local function handleNotify(subscr, data, headers)
gSubscriptionIsWorking = true
gSubscriptionIsTrying = false
local allGood, xmlOrError = pcall(xml.eval, data)
if not allGood then
debug.log(gFctName.." received Notify was not a valid XML: "..tostring(xmlOrError), "e")
debug.log(gFctName.." ignoring un-parsable notify: "..data, "e")
return
end
local dlgInfo = xmlOrError
if dlgInfo ~= nil then
debug.log(gFctName.." received Notify", "d")
if dlgInfo:tag() ~= "dialog-info" then
debug.log(gFctName..": did not receive a dialog-info, ignoring "..tostring(dlgInfo:tag()) , "e")
return
end
local dlgVers = dlgInfo.version
local dlgEntity = dlgInfo.entity
local dlgState = dlgInfo.state
if not dlgEntity or not dlgVers or not dlgState then
debug.log(gFctName..": received dialog-info is missing essencial attributes, ignoring dialog-info with "
..tostring(dlgVers).." / "..tostring(dlgEntity).." / "..tostring(dlgState) , "e")
return
end
local oldMetaInfos = {}
local newMetaInfos = {}
dlgVers = tonumber(dlgVers)
if not gDialogInfo then
-- assuming this is the first dialog-info we've received -> initialize gDialogInfo
gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
else
oldMetaInfos = gDialogInfo.metaInfos
if gDialogInfo.entity ~= dlgEntity then -- strange -> begin again
debug.log(gFctName..": entity in dialog-info changed, re-initializing gDialogInfo", "w")
gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
elseif gDialogInfo.version >= dlgVers then
debug.log(gFctName..": received outdated dialog-info -> ignoring version "..tostring(dlgVers), "i")
return
end
gDialogInfo.version = dlgVers
end
if dlgState == "full" then
gDialogInfo.dialogs = {}
end
for _,dlg in ipairs(dlgInfo) do
local dlgId = dlg.id
if dlgId then
gDialogInfo.dialogs[dlgId] = dlg
end
end
-- evaluate metaState:
local busyDlgs = {}
local pickableDlgs = {}
for dlgId, dlg in pairs(gDialogInfo.dialogs) do
newMetaInfos[dlgId]={}
newMetaInfos[dlgId].state = "terminated" -- might change below to ringing / calling / connected
local stateXml = dlg:find("state")
local state = nil
if stateXml and #stateXml >= 1 then state = stateXml[1] end
if state == "confirmed" then
busyDlgs[#busyDlgs+1] = dlgId
newMetaInfos[dlgId].state = "connected"
elseif state == "terminated" then
-- ignore
else
-- assuming proceeding, early oder trying
local direction = dlg.direction
if direction == "initiator" then
busyDlgs[#busyDlgs+1] = dlgId
newMetaInfos[dlgId].state = "calling"
else
pickableDlgs[#pickableDlgs+1] = dlgId
newMetaInfos[dlgId].state = "ringing"
end
end
if oldMetaInfos[dlgId] and oldMetaInfos[dlgId].state == newMetaInfos[dlgId].state then
newMetaInfos[dlgId].stateTime = oldMetaInfos[dlgId].stateTime
else
newMetaInfos[dlgId].stateTime = time.currentTimeMillis()
end
end
gDialogInfo.metaInfos = newMetaInfos
if #pickableDlgs > 0 then
gDialogInfo.activeDialogs = pickableDlgs
gDialogInfo.metaState = "pickable"
elseif #busyDlgs > 0 then
gDialogInfo.activeDialogs = busyDlgs
gDialogInfo.metaState = "busy"
else
gDialogInfo.activeDialogs = {}
gDialogInfo.metaState = "free"
end
debug.log(gFctName..": state from Notify: "..gDialogInfo.metaState, "d")
else
debug.log(gFctName..": received empty notify from "..subscr:getUri() , "e")
end
updateKey()
if gShowingPickupList or not gHideInMixedPickupScreen then
local pickupInfos = collectPickupList()
if pickupListChanged(gLastReportedPickupInfos, pickupInfos) then
system.updatePickupScreen{uri=pRemote, line=pIdentity, calls=pickupInfos, hideInMixedList=gHideInMixedPickupScreen}
gLastReportedPickupInfos = pickupInfos
end
end
end
local function unsubscribe()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = false
gDialogInfo = nil
if (gOngoingSubscription) then
gOngoingSubscription:unsubscribe()
gOngoingSubscription = nil
end
end
local function subscriptionTerminated(subscrObj, reason)
gSubscriptionIsWorking = false
gSubscriptionIsTrying = false
gDialogInfo = nil
if reason == "Account not found" or (reason == "Not Found" and gRemoteUser:sub(1,1) == "+") then
local cfgPath = getKeySettingsPath()
if cfgPath then
debug.log("Pbx reported surveiled account doesn't exist for "..gFctName.."-key -> disabling subscription", "e")
gWithSubscription = false
cfgPath = cfgPath.."/parameters/parameter[@name='@string/subscription_enabled']/@value"
config.set(cfgPath, "false")
end
else
debug.log(gFctName..": subscription failed with "..tostring(reason), "e")
end
updateKey()
end
local function onSubscrIsTrying()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = true
updateKey()
end
-- (re)subcribe to dialog-subscription. If there is already an ongoing subscrition -> trigger a re-subscribe on it.
-- When gWithSubscription == false -> will at most unsubscribe.
local function subscribe()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = false
gDialogInfo = nil
if gOngoingSubscription then
if gWithSubscription then
gOngoingSubscription:resubscribe()
else
gOngoingSubscription:unsubscribe()
gOngoingSubscription = nil
end
else
if gWithSubscription then
gOngoingSubscription = sip.subscribe{uri=pRemote, onNotify=handleNotify, line=pIdentity,
onTerminated=subscriptionTerminated, onTrying=onSubscrIsTrying}
end
end
end
local function getPresenceNote(xmlTree)
local firstNoteWithText = nil
for _,subTree in ipairs(xmlTree) do
local tag = subTree and xml.tag(subTree) or nil
if tag and tag == "note" then
local note = tostring(subTree[1])
local noteLowered = note:lower()
if noteLowered:find("offline") then
return "offline", note
elseif noteLowered:find("dnd") or noteLowered:find("disturb") then
return "dnd", note
elseif noteLowered:find("fwd") or noteLowered:find("forward") then
return "fwd", note
elseif noteLowered:find("away") then
return "away", note
elseif noteLowered:find("busy") or noteLowered:find("ring") or (noteLowered:find("on") and noteLowered:find("phone")) then
return "busy", note
elseif (noteLowered:find("available") or noteLowered:find("online") or noteLowered:find("idle") or (noteLowered:find("ready")) and not noteLowered:find("not ")) then
return "available", note
end
if firstNoteWithText == nil and note and #note > 0 then
firstNoteWithText = note
end
end
end
return "unknown", firstNoteWithText
end
local function handlePresenceNotify(subscr, data, headers)
gPresenceIsWorking = true
gPresenceIsTrying = false
local allGood, xmlOrError = pcall(xml.eval, data)
if not allGood then
debug.log(gFctName.." received presence notify was not a valid XML: "..tostring(xmlOrError), "e")
debug.log(gFctName.." ignoring un-parsable notify: "..data, "e")
return
end
local presenceXml = xmlOrError
if presenceXml ~= nil then
debug.log(gFctName.." received presence notify", "d")
if presenceXml:tag() ~= "presence" then
debug.log(gFctName..": did not receive a presence-tree, ignoring "..tostring(presenceXml:tag()) , "e")
return
end
local note = nil
local firstNoteWithText = nil
gPresenceState = "unknown"
local basic = xml.find(presenceXml, "basic")
local basicallyOpen = false
if basic then
if basic[1] == "closed" then
gPresenceState = "offline"
else
basicallyOpen = true
end
end
if gPresenceState == "unknown" then
gPresenceState, note = getPresenceNote(presenceXml)
if firstNoteWithText == nil and note and #note > 0 then
firstNoteWithText = note
end
end
if gPresenceState == "unknown" then
for _,subTree in ipairs(presenceXml) do
local tag = subTree and xml.tag(subTree) or nil
if tag and tag == "tuple"then
gPresenceState, note = getPresenceNote(subTree)
if firstNoteWithText == nil and note and #note > 0 then
firstNoteWithText = note
end
if gPresenceState ~= "unknown" then
goto onPresenceFound
end
end
end
end
if gPresenceState == "unknown" then
for _,subTree in ipairs(presenceXml) do
local tag = subTree and xml.tag(subTree) or nil
if tag and tag == "person" then
debug.log(gFctName.." found tree "..tag, "i")
gPresenceState, note = getPresenceNote(subTree)
if firstNoteWithText == nil and note and #note > 0 then
firstNoteWithText = note
end
if gPresenceState ~= "unknown" then
goto onPresenceFound
end
end
end
end
::onPresenceFound::
if gPresenceState == "unknown" then
if firstNoteWithText then
gPresenceStateString = firstNoteWithText
elseif basicallyOpen then
gPresenceState = "available"
gPresenceStateString = "@string/presence_available"
else
gPresenceStateString = "@string/presence_unknown"
end
else
gPresenceStateString = "@string/presence_"..gPresenceState
end
debug.log(gFctName..": state from presence notify: "..gPresenceState, "d")
else
debug.log(gFctName..": received empty presence notify from "..subscr:getUri() , "e")
end
updateKey()
end
local function unsubscribePresence()
gPresenceIsWorking = false
gPresenceIsTrying = false
if (gOngoingPresenceSubscr) then
gOngoingPresenceSubscr:unsubscribe()
gOngoingPresenceSubscr = nil
end
end
local function presenceTerminated(subscrObj, reason)
gPresenceIsWorking = false
gPresenceIsTrying = false
debug.log(gFctName..": presence failed with "..tostring(reason), "e")
updateKey()
end
local function onPresenceIsTrying()
gPresenceIsWorking = false
gPresenceIsTrying = true
updateKey()
end
local function subscribePresence()
unsubscribePresence()
if gWithPresence then
gOngoingPresenceSubscr = sip.subscribe{uri=pRemote, onNotify=handlePresenceNotify, line=pIdentity,
onTerminated=presenceTerminated, onTrying=onPresenceIsTrying,
type="presence"}
end
end
local function identityChanged(path)
gPickupCode = config.get(gUserpath .. "pickupCode")
gDoPickupWithReplaces = not gPickupCode or string.len(gPickupCode) == 0
local new_user = config.get(gUserpath .. "username")
local new_domain = sip.identities.getDomain(pIdentity);
if new_user ~= gUser or new_domain ~= domain then
unsubscribe()
gUser = new_user
gDomain = new_domain
subscribe()
subscribePresence()
updateKey()
end
end
local function setupRegChange()
config.register(gUserpath, identityChanged)
identityChanged()
end
function onStopShowingPickupList()
gShowingPickupList = false
end
function onKeyUp()
local headers = {}
debug.log(gFctName.."-key pressed", "d")
if not isValid() then
debug.log("missing parameter(s), "..gFctName.."-key won't work ever", "e")
return
elseif gLastReportedState == "presence_error" or gLastReportedState == "presence_trying" then
debug.log(gFctName..": presence-subscription had failed before, attempting restart", "d")
subscribePresence()
updateKey()
return
elseif gLastReportedState == "error" or gLastReportedState == "trying" then
debug.log(gFctName..": subscription had failed before, attempting restart", "d")
subscribe()
updateKey()
return
elseif gLastReportedState == "calling" then
debug.log(gFctName..": outgoing call thats not connected yet -> ignore key-press", "d")
return
elseif gLastReportedState == "active" then
debug.log(gFctName..": connected call -> hold", "d")
sip.calls.hold(gLocalCallId)
return
elseif gLastReportedState == "holding" then
debug.log(gFctName..": held call -> retrieve", "d")
sip.calls.resume(gLocalCallId)
return
elseif gLastReportedState == "ringing" then
debug.log(gFctName..": ringing call -> accept", "d")
sip.calls.accept(gLocalCallId)
return
elseif gWithPickup then
local pickupInfos = collectPickupList()
if getDictSize(pickupInfos) > 0 and gShowPickupScreenOnPress then
gShowingPickupList = true
system.showPickupScreen{uri=pRemote, line=pIdentity, calls=pickupInfos, onExit=onStopShowingPickupList}
gLastReportedPickupInfos = pickupInfos
return
end
if getDictSize(pickupInfos) > 0 or (gLastReportedState == "busy" and gPrevReportedState == "pickable") then
debug.log(gFctName..": picking the call", "d")
local doPickup = false
local replHdr, replVal = getFirstReplacesInfo()
if not gDoPickupWithReplaces then
debug.log(gFctName..": picking with pickup-code '"..gPickupCode.."'", "d")
doPickup = true
else
if replHdr then
debug.log(gFctName..": picking with Replaces-Header: "..replVal, "d")
headers[replHdr] = replVal
else
debug.log(gFctName..": cannot pick, replaces-info unavailable", "e")
return
end
end
sip.invite{uri=pRemote, line=pIdentity, hidden=false, headers=headers, pickup=doPickup}
return
end
end
debug.log(gFctName..": simply calling the remote (state: "..gLastReportedState..", autoAnswer: "..tostring(gWithAutoAnswer)..")", "d")
if gWithAutoAnswer then
headers['Alert-Info'] = '<http://127.0.0.1>;info=alert-autoanswer'
end
sip.invite{uri=pRemote, line=pIdentity, hidden=false, headers=headers}
end
function initialize()
key:setInfo("initializing")
if isValid() then
setupRegChange()
else
debug.log("missing parameters, "..gFctName.."-key won't work", "e")
updateKey()
return
end
gRemoteUri = sipTools.parseUri{sipUri=pRemote, fromUserInput=true, idIdx=pIdentity}
gRemoteUser = gRemoteUri:getUser()
if gWithMissed then
calllog.monitorMissed{onNewMissed=onDbResult, line=pIdentity, user=gRemoteUser}
end
sip.calls.listen{callback=onCallEvent, line=pIdentity, user=gRemoteUser}
gFctName = "BLF["..gRemoteUri:getUiNumber().." on id "..pIdentity.."]"
local cfgPath = getKeySettingsPath()
if cfgPath then
gLabel = config.get(cfgPath.."/@label")
end
if gWithPresence then
sip.setupSubscriptionType{name="presence", accepted="application/pidf+xml"}
end
updateKey()
end
initialize()]]></code>
<params>
<param name="pRemote"/>
<param name="pIdentity"/>
<param name="pSubscriptionEnabled"><value>true</value></param>
<param name="pDoPickup"><value>true</value></param>
<param name="pRequestAutoAnswer"><value>false</value></param>
<param name="pBlinkOnMissed"><value>true</value></param>
<param name="pShowPickupScreenOnPress"><value>false</value></param>
<param name="pHideInMixedPickupScreen"><value>false</value></param>
<param name="pPickupConnected"><value>false</value></param>
<param name="pSubscribePresence"><value>false</value></param>
</params>
</lua>
</keyConfiguration>
<parameters>
<parameter name="@string/uri" type="sip_uri">
<path>//param[@name="pRemote"]/value</path>
</parameter>
<parameter optional="true" name="@string/identity" type="identity">
<path>//param[@name="pIdentity"]/value</path>
</parameter>
<parameter optional="true" name="@string/subscription_enabled" type="boolean">
<path>//param[@name="pSubscriptionEnabled"]/value</path>
</parameter>
<parameter optional="true" name="@string/do_pickup" type="boolean">
<path>//param[@name="pDoPickup"]/value</path>
</parameter>
<parameter optional="true" name="@string/auto_answer" type="boolean">
<path>//param[@name="pRequestAutoAnswer"]/value</path>
</parameter>
<parameter optional="true" name="@string/with_calllog" type="boolean">
<path>//param[@name="pBlinkOnMissed"]/value</path>
</parameter>
<parameter optional="true" name="@string/pickup_list_on_press" type="boolean">
<path>//param[@name="pShowPickupScreenOnPress"]/value</path>
</parameter>
<parameter optional="true" name="@string/pickup_list_hide" type="boolean">
<path>//param[@name="pHideInMixedPickupScreen"]/value</path>
</parameter>
<parameter optional="true" name="@string/pickup_connected" type="boolean">
<path>//param[@name="pPickupConnected"]/value</path>
</parameter>
<parameter optional="true" name="@string/signal_presence" type="boolean">
<path>//param[@name="pSubscribePresence"]/value</path>
</parameter>
</parameters>
</template>
<template prio="20" name="@string/line_title" label="@param-@string/identity" icon="@drawable/line">
<keyConfiguration>
<line>
<id></id>
</line>
</keyConfiguration>
<parameters>
<parameter name="@string/identity" type="identity">
<path>//line/id</path>
</parameter>
</parameters>
</template>
<template prio="10" name="@string/park_position" label="@param-@string/uri" icon="@drawable/park_orbit">
<keyConfiguration>
<lua>
<code><![CDATA[local gUserpath = "/identities/identity["..pIdentity.."]/"
local gSubscriptionIsTrying = false
local gSubscriptionIsWorking = false
local gUser = "" -- username of associated identity
local gDomain = "" -- domain (registrar) of associated identity
local gPickupCode = nil -- pickup-code of associated identity
local gDoPickupWithReplaces = false -- whether or not to use a replaces header (instead of pickup-code) to pick a call
local gRemoteUri = nil -- sipUri-object of remote
local gRemoteUser = nil -- user-part of remote uri
local gOngoingSubscription = nil
-- remember what we're signaling to the user, this will influence what is expected when key is pressed
-- one of:
-- - errors-state: error, trying
-- - subscription-event: idle, pickable
-- - nothing going on: idle
local gLastReportedState = nil
local gLocalCall = nil -- when there is an active call using same identity as this orbit -> store it here in case user decides to park it
local gFctName = "ParkOrbit" -- name for this key in logs (uri and identity will be added during initialization)
-- keep infos of all monitored calls on our remote
-- contains these entries:
-- - "entity" : attribute from dialog-info-xml
-- - "version" : attribute from dialog-info-xml
-- - "metaState" : one of free/pickable -> combined meta-state accumulated from all dialogs (monitored calls)
-- - "activeDialogs" : array of all dialog-ids of the all dialogs responsible for signaled meta-state.
-- - "dialogs" : map by dialog-id of all dialogs (monitored calls)
local gDialogInfo = nil
local function isValid() -- reports if key has all it's params set to valid values. Set to nil to keep this default
if pIdentity == "" or pIdentity == nil then
return false
end
if pRemote == "" or pRemote == nil then
return false
end
return true
end
-- Create value for replaces-header for first pickable dialog
local function getFirstReplacesInfo()
if #gDialogInfo.activeDialogs == 0 or not gDialogInfo.metaState == "pickable" then
debug.log(gFctName..": cannot find pickable call for replaces", "e")
return nil
end
local dlgXml = gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]]
local callId = dlgXml["call-id"]
if not callId or string.len(callId) == 0 then
debug.log(gFctName..": cannot find call-id for replaces-header", "e")
return nil
end
return sipTools.mkReplaces{callId=callId, to=dlgXml["remote-tag"], from=dlgXml["local-tag"]}
end
-- analyze existing dialogs and find a text that describes who our monitored remote is talking to
local function getBlfRemoteText()
if #gDialogInfo.activeDialogs == 0 then
return nil
elseif #gDialogInfo.activeDialogs > 1 and not gDoPickupWithReplaces then
-- wouldn't know which one would be picked
return "@string/multiple_calls"
end
-- find something to name the pick-party:
local remote = gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]]:find("remote")
if not remote then
debug.log(gFctName..": missing <remote> within <dialog-info>, cannot name remote party", "w")
return "@string/call_unknown"
end
local dispName = remote:find("identity", "display")
if dispName and string.len(dispName.display) > 0 then return dispName.display end
local remote_usr = remote:find("identity")
if not remote_usr then remote_usr = remote:find("target") end
if not remote_usr or not remote_usr[1] then
debug.log(gFctName..": missing remote-uri within <dialog-info>, cannot name remote party", "w")
return "@string/call_unknown"
end
if string.find(string.lower(remote_usr[1]), "anonymous") then
return "@string/call_anonyme"
end
local remote_uri = sipTools.parseUri{sipUri=remote_usr[1]}
if not remote_uri or not remote_uri:getUiNumber() then
return remote_usr[1]
end
return remote_uri:getUiNumber()
end
local function updateKey()
local desiredTitle = ""
local desiredInfo = gRemoteUser
local desiredIcon = nil
if not isValid() then
gLastReportedState = "error"
key:setLed("orange", false)
desiredInfo = "@string/setup_failed"
desiredIcon = "@drawable/block"
else
if gSubscriptionIsWorking then
if gDialogInfo.metaState == "pickable" then
gLastReportedState = "pickable"
key:setLed("red", false)
desiredInfo = getBlfRemoteText()
else
gLastReportedState = "idle"
key:setLed("off")
end
elseif gSubscriptionIsTrying then
gLastReportedState = "trying"
key:setLed("orange", true)
desiredInfo = "@string/subscription_trying"
else
gLastReportedState = "error"
key:setLed("orange", false)
desiredInfo = "@string/subscription_failed"
end
end
key:setIcon(desiredIcon)
key:setTitle(desiredTitle)
key:setInfo(desiredInfo)
end
function getLocalCall(state)
local allCalls = sip.calls.get_all()
for _,aCall in ipairs(allCalls) do
if aCall:getState() == state and tostring(aCall:getIdentityIndex()) == pIdentity then
return aCall
end
end
return nil
end
function onCallEvent(event, call1, call2)
-- save the currently active call so we have it in case user wants to park it
local theCall = getLocalCall("active")
if not theCall then -- no active call .. maybe there is an incoming ringing one
theCall = getLocalCall("ringing")
end
gLocalCall = theCall
end
local function createDialogInfoStruct(dlgVers, dlgEntity)
local result = {}
result.entity = dlgEntity
result.version = dlgVers
result.metaState = "free"
result.activeDialogs = {}
result.dialogs = {}
return result
end
local function handleNotify(subscr, data, headers)
-- parse remote party and state (trying, confirmed or other)
gSubscriptionIsWorking = true
gSubscriptionIsTrying = false
local allGood, xmlOrError = pcall(xml.eval, data)
if not sd then
debug.log(gFctName.." received Notify was not a valid XML: "..tostring(xmlOrError), "e")
debug.log(gFctName.." ignoring un-parsable notify: "..data, "e")
return
end
local dlgInfo = xmlOrError
if dlgInfo ~= nil then
debug.log(gFctName.." received Notify", "d")
if dlgInfo:tag() ~= "dialog-info" then
debug.log(gFctName..": did not receive a dialog-info, ignoring "..tostring(dlgInfo:tag()) , "e")
return
end
local dlgVers = dlgInfo.version
local dlgEntity = dlgInfo.entity
local dlgState = dlgInfo.state
if not dlgEntity or not dlgVers or not dlgState then
debug.log(gFctName..": received dialog-info is missing essencial attributes, ignoring dialog-info with "
..tostring(dlgVers).." / "..tostring(dlgEntity).." / "..tostring(dlgState) , "e")
return
end
dlgVers = tonumber(dlgVers)
if not gDialogInfo then
-- assuming this is the first dialog-info we've received -> initialize gDialogInfo
gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
else
if gDialogInfo.entity ~= dlgEntity then -- strange -> begin again
debug.log(gFctName..": entity in dialog-info changed, re-initializing gDialogInfo", "w")
gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
elseif gDialogInfo.version >= dlgVers then
debug.log(gFctName..": received outdated dialog-info -> ignoring version "..tostring(dlgVers), "i")
return
end
gDialogInfo.version = dlgVers
end
if dlgState == "full" then
gDialogInfo.dialogs = {}
end
for _,dlg in ipairs(dlgInfo) do
local dlgId = dlg.id
if dlgId then
gDialogInfo.dialogs[dlgId] = dlg
end
end
-- evaluate metaState:
local busyDlgs = {}
local pickableDlgs = {}
for dlgId, dlg in pairs(gDialogInfo.dialogs) do
local stateXml = dlg:find("state")
local state = nil
if stateXml and #stateXml >= 1 then state = stateXml[1] end
if state ~= "terminated" then
-- detailed state doesn't matter in park-orbits, it'll likely only contain confirmed dialogs and
-- whatever the state, each call should be pickable
pickableDlgs[#pickableDlgs+1] = dlgId
end
end
if #pickableDlgs > 0 then
gDialogInfo.activeDialogs = pickableDlgs
gDialogInfo.metaState = "pickable"
else
gDialogInfo.activeDialogs = {}
gDialogInfo.metaState = "free"
end
debug.log(gFctName..": state from Notify: "..gDialogInfo.metaState, "d")
else
debug.log(gFctName..": received empty notify from "..subscr:getUri() , "e")
end
updateKey()
end
local function unsubscribe()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = false
gDialogInfo = nil
if (gOngoingSubscription) then
gOngoingSubscription:unsubscribe()
gOngoingSubscription = nil
end
end
local function subscriptionTerminated()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = false
gDialogInfo = nil
updateKey()
end
local function onSubscrIsTrying()
gSubscriptionIsWorking = false
gSubscriptionIsTrying = true
updateKey()
end
local function subscribe()
unsubscribe()
gOngoingSubscription = sip.subscribe{uri=pRemote, onNotify=handleNotify, line=pIdentity,
onTerminated=subscriptionTerminated, onTrying=onSubscrIsTrying}
end
local function identityChanged(path)
gPickupCode = config.get(gUserpath .. "pickupCode")
gDoPickupWithReplaces = not gPickupCode or string.len(gPickupCode) == 0
local new_user = config.get(gUserpath .. "username")
local new_domain = sip.identities.getDomain(pIdentity);
if new_user ~= gUser or new_domain ~= domain then
unsubscribe()
gUser = new_user
gDomain = new_domain
subscribe()
updateKey()
end
end
local function setupRegChange()
config.register(gUserpath, identityChanged)
identityChanged()
end
function onKeyUp()
debug.log(gFctName.."-key pressed", "d")
if not isValid() then
debug.log("missing parameter(s), "..gFctName.."-key won't work ever", "e")
return
elseif gLastReportedState == "error" then
debug.log(gFctName..": subscription had failed before, attempting restart", "d")
subscribe()
updateKey()
return
elseif gLastReportedState == "trying" then
debug.log(gFctName..": subscription is being tried: stopping and restarting", "d")
unsubscribe()
subscribe()
updateKey()
return
elseif gLastReportedState == "pickable" then
debug.log(gFctName..": picking the call", "d")
sip.invite{uri=pRemote, line=pIdentity, callLogNr=2}
elseif gLocalCall then
debug.log(gFctName..": parking call to "..gLocalCall:getRemote().." on orbit "..gRemoteUser, "d")
gLocalCall:transfer(gRemoteUser)
else
debug.log(gFctName..": no call found that could be parked", "d")
end
end
function initialize()
if isValid() then
setupRegChange()
else
debug.log("missing parameters, "..gFctName.."-key won't work", "e")
updateKey()
return
end
gRemoteUri = sipTools.parseUri{sipUri=pRemote, fromUserInput=true, idIdx=pIdentity}
gRemoteUser = gRemoteUri:getUser()
sip.calls.listen(onCallEvent)
gFctName = "ParkOrbit["..gRemoteUri:getUiNumber().." on id "..pIdentity.."]"
updateKey()
end
initialize()]]></code>
<params>
<param name="pRemote"/>
<param name="pIdentity"/>
</params>
</lua>
</keyConfiguration>
<parameters>
<parameter name="@string/uri" type="sip_uri">
<path>//param[@name="pRemote"]/value</path>
</parameter>
<parameter optional="true" name="@string/identity" type="identity">
<path>//param[@name="pIdentity"]/value</path>
</parameter>
</parameters>
</template>
<template prio="10" name="@string/do_not_disturb_title" icon="@drawable/dnd">
<keyConfiguration>
<function>
<invocations>
<invocation>
<setting path="/telephony/doNotDisturb/active">
<values>
<value>true</value>
<led ledColor="red"/>
</values>
<values>
<value>false</value>
<led ledColor="off"/>
</values>
</setting>
</invocation>
</invocations>
</function>
</keyConfiguration>
</template>
<template prio="10" name="@string/call_forwarding_unconditional_title" icon="@drawable/call_forwarding">
<keyConfiguration>
<lua>
<code>
pathPrefix = "/telephony/callForwarding/unconditional/"
require "callforward"
</code>
</lua>
</keyConfiguration>
</template>
<template prio="10" name="@string/call_forwarding_busy_title" icon="@drawable/call_forwarding">
<keyConfiguration>
<lua>
<code>
pathPrefix = "/telephony/callForwarding/busy/"
require "callforward"
</code>
</lua>
</keyConfiguration>
</template>
<template prio="10" name="@string/call_forwarding_no_response_title" icon="@drawable/call_forwarding">
<keyConfiguration>
<lua>
<code>
pathPrefix = "/telephony/callForwarding/noResponse/"
require "callforward"
</code>
</lua>
</keyConfiguration>
</template>
<template prio="10" name="@string/http_request_title" label="@param-@string/web_url" icon="@drawable/http_request">
<keyConfiguration>
<function>
<invocations>
<invocation>
<http>
<request httpMethod="get">
<uri></uri>
</request>
<httpReactions>
<responseCode ledColor="off">
<responseCode>200</responseCode>
</responseCode>
</httpReactions>
</http>
</invocation>
</invocations>
</function>
</keyConfiguration>
<parameters>
<parameter name="@string/web_url" type="web_uri">
<path>//uri</path>
</parameter>
</parameters>
</template>
<template prio="10" name="@string/call_waiting_title" icon="@drawable/call_waiting">
<keyConfiguration>
<function>
<invocations>
<invocation>
<setting path="/telephony/callWaiting/active">
<values>
<value>true</value>
<led ledColor="green"/>
</values>
<values>
<value>false</value>
<led ledColor="off"/>
</values>
</setting>
</invocation>
</invocations>
</function>
</keyConfiguration>
</template>
<template prio="10" name="@string/clir_title" icon="@drawable/clir">
<keyConfiguration>
<function>
<invocations>
<invocation>
<setting path="/telephony/clir/active">
<values>
<value>true</value>
<led ledColor="green"/>
</values>
<values>
<value>false</value>
<led ledColor="off"/>
</values>
</setting>
</invocation>
</invocations>
</function>
</keyConfiguration>
</template>
<template prio="10" name="@string/vpn" icon="@drawable/setting">
<keyConfiguration>
<lua>
<code><![CDATA[local settingPath = "network/vpn/active"
local active = config.get( settingPath )
local vpnGoodViaNotification = false
local vpnGoodViaNetwork = false
local vpnHaveVpnNotis = false
local vpnNotis = {}
local function updateLed()
if active == "true" then
if vpnGoodViaNotification or vpnGoodViaNetwork then
key:setLed( "green" )
elseif vpnHaveVpnNotis then
key:setLed( "orange", true )
else
key:setLed( "orange" )
end
else
key:setLed( "off" )
end
end
local function configListener( path )
active = config.get( settingPath )
updateLed()
end
function onKeyUp()
if active == "false" then
debug.log("turning VPN on")
config.set( settingPath, "true" )
else
debug.log("turning VPN off")
config.set( settingPath, "false" )
end
end
local function onSuccessNotiConnected()
vpnGoodViaNotification = true;
updateLed()
end
local function onSuccessNotiConnectedStopped()
vpnGoodViaNotification = false;
updateLed()
end
local function onNotiConnected(note)
vpnNotis[note:getId()] = true
vpnHaveVpnNotis = true
updateLed()
end
local function onNotiConnectedStopped(note)
vpnNotis[note:getId()] = false
-- one note was stopped but maybe another vpn-note is still active..
local stillHaveVpn = false
for _,isActive in pairs(vpnNotis) do
if isActive then
stillHaveVpn = true
break
end
end
vpnHaveVpnNotis = stillHaveVpn
updateLed()
end
local function onVpnEstablished()
vpnGoodViaNetwork = true;
updateLed()
end
local function onVpnLost()
vpnGoodViaNetwork = false;
updateLed()
end
system.notifications.monitor{onCreated=onSuccessNotiConnected, onDeleted=onSuccessNotiConnectedStopped, pkg=system.notifications.constants.vpnPkg, id=system.notifications.constants.vpnIdSuccess}
system.notifications.monitor{onCreated=onNotiConnected, onDeleted=onNotiConnectedStopped, pkg=system.notifications.constants.vpnPkg}
system.network.monitor{onAvailable=onVpnEstablished, onLost=onVpnLost, transports="vpn"}
config.register( settingPath, configListener )
updateLed()--]]></code>
</lua>
</keyConfiguration>
</template>
<template prio="10" name="@string/dtmf_relay_title" label="@param-@string/dtmf_sequence" icon="@drawable/relay">
<keyConfiguration>
<lua>
<code>
function onKeyUp()
sip.calls.dtmf{dtmf = dtmf_sequence}
end
</code>
<params>
<param name="dtmf_sequence"/>
</params>
</lua>
</keyConfiguration>
<parameters>
<parameter name="@string/dtmf_sequence" type="phone">
<path>//param[@name="dtmf_sequence"]/value</path>
</parameter>
</parameters>
</template>
<template name="@string/silent_alert" prio="10" icon="@drawable/bluelight">
<keyConfiguration>
<lua>
<code>
local silentAlertNotifyId = 24242
local function onClickedNotify()
http.get{url=url, verifyHostname=false, acceptUnsignedCerts=true}
local notification = system.notifications.get{id=silentAlertNotifyId}
if notification then
notification:delete()
end
end
function onKeyUp()
if url ~= nil and url ~= "" and delay ~= nil then
if delay ~= "" and tonumber(delay) > 0 then
system.notifications.add{tag="SilentAlertTag", id=silentAlertNotifyId, title="@string/silent_alert",
message="@string/silent_alert_msg", autoClickDelay=tonumber(delay),
alert=true, alertSound=false, alertOkText="@string/silent_alert_ok", alertCancelText="",
onClicked=onClickedNotify}
else
http.get{url=url, verifyHostname=false, acceptUnsignedCerts=true}
end
end
end
</code>
<params>
<param name="url"/>
<param name="delay">
<value>10</value>
</param>
</params>
</lua>
</keyConfiguration>
<parameters>
<parameter name="@string/silent_alert_url" type="web_uri">
<path>//param[@name="url"]/value</path>
</parameter>
<parameter name="@string/silent_alert_delay" type="number" min="0" max="60">
<path>//param[@name="delay"]/value</path>
</parameter>
</parameters>
</template>
<template name="@string/sip_message" prio="10" icon="@drawable/message_outline" label="@param-@string/param_message">
<keyConfiguration>
<lua>
<code><![CDATA[
local TAG = "SipMsg-countdown from "..key.MODULE..":"..key.INDEX
local mOngoingAlertingId = nil
local function removeOngoingNotification()
if mOngoingAlertingId then
local notification = system.notifications.get{tag=TAG, id=mOngoingAlertingId}
if notification then
notification:delete()
end
mOngoingAlertingId = nil
end
end
local function onClicked()
sip.sendMessage{line=identity, message=msg, target=url}
removeOngoingNotification()
end
function onKeyUp()
if url ~= nil and url ~= "" and delay ~= nil then
if delay ~= "" and tonumber(delay) > 0 then
removeOngoingNotification()
mOngoingAlertingId = system.notifications.getUniqueId()
system.notifications.add { tag=TAG, id=mOngoingAlertingId, title="@string/sip_message", onClicked=onClicked,
message=msg.."\n@string/silent_alert_msg", autoClickDelay=tonumber(delay), alert=true,
alertSound=false, alertOkText="@string/silent_alert_ok", alertCancelText="",
icon="@drawable/notification_icon_voice_bubble"}
else
sip.sendMessage{line=identity, message=msg, target=url}
end
end
end]]></code>
<params>
<param name="url"/>
<param name="msg"/>
<param name="identity"/>
<param name="delay">
<value>0</value>
</param>
</params>
</lua>
</keyConfiguration>
<parameters>
<parameter name="@string/uri" type="sip_uri">
<path>//param[@name="url"]/value</path>
</parameter>
<parameter name="@string/identity" type="identity">
<path>//param[@name="identity"]/value</path>
</parameter>
<parameter name="@string/param_message" type="text">
<path>//param[@name="msg"]/value</path>
</parameter>
<parameter optional="true" name="@string/silent_alert_delay" type="number" min="0" max="60">
<path>//param[@name="delay"]/value</path>
</parameter>
</parameters>
</template>
<template name="@string/pickup_app_name" icon="@drawable/pickup_fkey">
<keyConfiguration>
<lua>
<code><![CDATA[
function onKeyUp()
system.intent{action="auerswald.action.SHOW_PICKUP_LIST", component="de.auerswald.home"}
end
local function onHavePickups(havePickups)
if havePickups then
key:setLed("red", true)
else
key:setLed("off")
end
end
system.monitorHavePickups{onHavePickupsChanged=onHavePickups};
]]></code>
</lua>
</keyConfiguration>
</template>
</templates>