Hi there.
Introduction
This is a follow-up to my presence detection solution I mentioned here. This solution adds text-to-speech announcements via Sonos speakers as soon as someone arrives. I have also added extensive comments to the code. It is a full example that should contain everything you need to get going.
Requirements:
- Expire Binding (binding-expire1)
- Fritzbox TR064 Binding (binding-fritzboxtr0641). Announcements will work with any other presence detection system as long as the presence items are properly updated.
- Map Transformation (transformation-map). Just for display.
- Speakers & their binding (I use Sonos)
- Audio sink is configured to whatever speaker system you’re using
- Text-to-speech engine is configured. I use Mary Text-to-Speech Engine that comes free with openHab. It produces reasonable results and runs fine on a Raspberry 3 (see comments).
The Presence.items file
/*
* Presence detection using Fritz TR064 and Expire bindings.
*
* I have found that the Fritz!Box TR064 presence detection works quite well
* but there are a few things to know. Presence is reported almost instantly
* but absence is reported with an approx. 10 to 15 min delay. I suppose this was
* done on purpose by AVM to avoid devices constantly jumping back and forth between
* presence states. Even so, our iPhones are being reported as absent seemingly at
* random once or twice a day for max. 5 minutes without apparent reason.
* As a consequence I set the TR064 binding to an update interval of 1min
* (reports presence very quickly) and I am using the expire binding to let the
* devices jump to absent with a 5min delay. For me this works OK in practice.
* But unfortunately it means that there is an up to 20min delay until someone is
* reported as being absent. This is important to know.
*/
/*
* First define the groups
*/
// Does nothing, just brings order to chaos
Group PresenceDetection "Presence Detection"
// If at least one person is present (ON), the group is present (ON), else absent (OFF)
Group:Switch:OR(ON, OFF) PresenceAnyone "Anyone [MAP(presence.map):%s]" <present> (PresenceDetection)
// If all are present (ON), the group is present (ON), else absent (OFF)
Group:Switch:AND(ON, OFF) PresenceEveryone "Everyone [MAP(presence.map):%s]" <present> (PresenceDetection)
/*
* Then define one presence item per person (cell phone) using the TR064 binding. These
* are updated once per minute by the Fritz!Box. We use those items solely to update the
* ones created further below. We do not use an expire binding because the TR064 binding
* switches between ON and OFF.
*/
// iPhone 1
Switch PresenceFritzJohn "John [MAP(presence.map):%s]" <present> (PresenceDetection) {fritzboxtr064="maconline:xx-xx-xx-xx-xx-xx"}
// iPhone 2
Switch PresenceFritzSally "Sally [MAP(presence.map):%s]" <present> (PresenceDetection) {fritzboxtr064="maconline:xx-xx-xx-xx-xx-xx"}
/*
* Define presence items that are actually used by our code. They are set to ON once
* the related Fritz item changes to ON. Using the expire binding they fall back to
* OFF (not present) after 5mins without update. In other words they are not set to OFF
* by the TR064 binding but by timeout.
* We need to do this due to a weird behavior of the Fritz!Box presence reporting. As soon
* as a device is reported being absent (OFF) it immediately reappears (ON) for a few
* seconds only to (finally) switch back to absent. It then stays absent until the device
* actually returns to the network.
* Once AVM fixes this we only need the items defined above.
*/
Switch PresenceJohn "John [MAP(presence.map):%s]" <present> (PresenceAnyone, PresenceEveryone) {expire="5m,OFF"}
Switch PresenceSally "Sally [MAP(presence.map):%s]" <present> (PresenceAnyone, PresenceEveryone) {expire="5m,OFF"}
/*
* Lastly, define arrival items signaling that someone is arriving. Arrival means
* that someone went from absent to present within the last five minutes. I use
* these to switch on welcome lights. Again, the expire binding comes in handy and
* saves us from having to use timers.
* Sugar groups enable useful features such as doing something if the first person
* comes home to an empty house.
*/
// If at least one person is arriving (ON), anyone is arriving (ON) else not (OFF)
Group:Switch:OR(ON, OFF) ArrivalAnyone "Anyone [MAP(arrival.map):%s]" (PresenceDetection)
// Updated by rule
Switch ArrivalFirst "First [MAP(arrival.map):%s]" (PresenceDetection) {expire="5m,OFF"}
// Status "arriving" is kept for five minutes
Switch ArrivalJohn "John [MAP(arrival.map):%s]" (ArrivalAnyone) {expire="5m,OFF"}
Switch ArrivalSally "Sally [MAP(arrival.map):%s]" (ArrivalAnyone) {expire="5m,OFF"}
The Sonos.items file
/*
* SONOS
*
* Note: I own one pair of PLAY:1 configured as a stereo pair. RINCON_xxx seems to be the master
* of my stereo pair so we use this one. Maybe it doesn't matter which one to use.
*/
// The volume of the music streaming channel
Dimmer SonosLivingRoomVolume {channel="sonos:PLAY1:RINCON_xxx:volume"}
/*
* The volume of the notifications channel. The say command writes to the
* notifications channel. As per doc (*): There is a limit of 20s per notification.
* Everything after that is cut off.
* (*) http://docs.openhab.org/addons/bindings/sonos/readme.html#audio-support
*/
Dimmer SonosLivingRoomNotificationVolume {channel="sonos:PLAY1:RINCON_xxx:notificationvolume"}
The Presence.rules file
rule "System Start"
when
// Also triggered upon saving of file
System started
then
// Assume everyone is home to not trigger alarms at system start
PresenceAnyone.members.forEach[ SwitchItem item |
item.postUpdate(ON)
]
// Assume no one is arriving at system start
ArrivalAnyone.members.forEach[ SwitchItem item |
item.postUpdate(OFF)
]
end
/*
* Evaluate TR064 events and set the states of the "real" presence items.
* We need this workaround to ignore a glitch in the TR064 presence
* detection where devices are sometimes reported as present for a few seconds
* immediately after having been reported as being absent. After that they
* stay absent indefinitely until the device is in fact present again.
*/
rule "John Present"
when
Item PresenceFritzJohn received update ON
then
PresenceJohn.postUpdate(ON)
end
rule "Sally Present"
when
Item PresenceFritzSally received update ON
then
PresenceSally.postUpdate(ON)
end
/*
* Set the status of the arrival items. Ignore changes from NULL to ON
* which happen at system start.
*
* Unfortunately we need one rule per person to distinguish between the
* individual presence item change events.
*/
rule "John Arriving"
when
Item PresenceJohn changed from OFF to ON
then
ArrivalJohn.postUpdate(ON)
end
rule "Sally Arriving"
when
Item PresenceSally changed from OFF to ON
then
ArrivalSally.postUpdate(ON)
end
/*
* No XOR available for groups. Update the item we use instead.
*/
rule "First Arrival"
when
Item PresenceAnyone changed from OFF to ON
then
ArrivalFirst.postUpdate(ON)
end
The Announcements.rules file
import org.eclipse.xtext.xbase.lib.Procedures
import java.util.Set
import java.util.HashSet
import java.util.Iterator
val Integer LEVEL_INFO = 0
val Integer LEVEL_WARN = 1
/*
* Announces the given text using the voice synthesizer configured
* in openHab. An additional level defines the criticality of the
* message (INFO or WARNING).
* We use a Procedure because it takes arguments (like a Function)
* but unlike a Function by definition returns nothing.
*/
val Procedures.Procedure2<Integer, String> announce = [ level, text |
var String message
switch level {
/*
* Cannot use the global "LEVEL_" values here. Need to put
* magic numbers instead. :-/
*/
case 0:
// Just use the text as-is
message = text
case 1:
// Add a prefix to make it clear this announcement is a warning
message = "Warning! " + text
default: {
logError("Announcements", "Unexpected value for level: " + level)
return
}
}
/*
* Set the announcement volume of whatever audio item is used. There
* might be timing issues that have to be dealt with. Also you
* might want to set the volume back to what it was before the
* announcement later on.
*/
SonosLivingRoomNotificationVolume.sendCommand(30)
/*
* Optional: Play announcement sound of your choice (ba-ding! bleep-boop!).
* Your chance of finally putting that Star Trek computer sound .wav to use
* you have recorded from a VHS tape with your brand new SoundBlaster 16
* some 20 odd years ago.
*/
playSound("announcement.mp3")
/*
* Send message to voice synthesizer. I have found that "Mary Text-to-Speech"
* works quite well on a Raspberry 3 once the various voice snippets have been
* generated and cached. Until then announcements might get delayed a bit.
*/
say(message)
logInfo("Announcements", "openHab says: " + message)
]
/*
* Produces a listing of the given set's elements as a single String. The elements
* are separated by commas. Except for the last one which is separated by "and".
* Used to construct anouncements.
*
* Examples:
* Rachel, Peter, Max --> "Rachel, Peter and Max"
* Rachel, Peter --> "Rachel and Peter"
* Rachel --> "Rachel"
*/
val Functions.Function1<Set<String>, String> listify = [ set |
if (set.empty) {
logWarn("Function listify", "Didn't expect to receive empty set of names.")
return ""
}
var String name
val Iterator<String> iter = set.iterator
// start with the first entry
var String list = iter.next
while(iter.hasNext) {
// look ahead
name = iter.next
if (iter.hasNext) {
list += ", " + name
} else {
list += " and " + name
}
}
return list
]
rule "ArrivalAnyone"
when
Item ArrivalAnyone changed to ON
then
// Set of people already present (excluding arriving)
val Set<String> present = new HashSet()
// Set of people that are absent (including arriving)
val Set<String> absent = new HashSet()
// Set of people that are arriving
val Set<String> arriving = new HashSet()
// Populate the set of names of people currently present and absent
PresenceAnyone.members.forEach[ SwitchItem item |
if (item.state == ON) {
present.add(item.label)
} else {
absent.add(item.label)
}
]
// Populate the set of people arriving
ArrivalAnyone.members.forEach[ SwitchItem item |
//logInfo("Announcements", item.label + " " + item.state)
if (item.state == ON) {
arriving.add(item.label)
// remove arriving from presence list
present.remove(item.label)
}
]
// Plausibility check
if (arriving.empty) {
/*
* No one is arriving - nothing to announce.
* This should not happen except for in test cases.
*/
logWarn("Announcements", "Skipping announcement. No one is arriving.")
return
}
// Greet the persons arriving
var String text = "Welcome home, " + listify.apply(arriving) + "."
// Give report on who was already there
switch count : present.size {
// .size can never be < 0
case 0:
logDebug("Announcements", "Skipping. No one there.")
case 1:
text += " " + present.get(0) + " is already there."
case count > 1:
text += " " + listify.apply(present) + " are already there."
default:
logError("Announcements", "Unexpected value: " + count)
}
announce.apply(LEVEL_INFO, text)
end
The presence.map file
NULL=undefined
ON=Present
OFF=Absent
The arrival.map file
NULL=undefined
ON=arrives
OFF=does not arrive
Edits
2017-11-27:
Corrected: Presence.rules contained an error where the actual presence items were only updated when the Fritz items “changed from OFF to ON”. However, it must be “received update ON” to continuously update the actual presence items so that they do not fall back to OFF after 5mins (due to the expire binding).
Also changed one sendCommand() to postUpdate() (copy & paste error).
2018-03-25:
Added space before first parameter of lambdas. See: