Presence detection and dynamic voice notification upon arrival (Fritz!Box, Sonos)

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:

5 Likes

Who told you you what sound file I used to tell the arrival of me?
Some experience from the usage of such arrival notifications:

We got a nosed after a week!, and turned it off.
At least use either sound or text!

The IPHONES “deep sleep” is causing it’s different behaviour, search the forum, there are a other solutions, if you remain interested.

On the setup of Sonos and the TTS, you have obviously set the desired box as the default audio sink (users with more then one audio sink have to set the sink in the play and say command.). The setup of the TTS might be worthwhile posting in the tutorial as well.

Other then that, THANK YOU for that tutorial.

1 Like

I love the vast amount of valuable information within your rules and how it all works together. Just wanted to look over and slurp up your knowledge. It never came to my mind that we can play a sound before the announcements. That totally brings me back to the Half-Life 1 announcement guy, so I guess I know that I will do this weekend :smiley: Thank you!

1 Like

Thanks for your comment! You are absolutely right. This is probably not for everyone. :slight_smile: I also meant it as a proof of concept of presence detection and what can be done with it (and how). So it’s mostly a “because I can!” thing. Perhaps a disclaimer is in order: May severely lower the woman acceptance factor. :slight_smile:

Concerning the iPhones’ presence: I found that everything breaks if they enter battery saving mode. So it is a good idea to always keep them either charged or (especially at night) keep them plugged in so the do not enter battery saving mode. Also it is a good idea to wake the iPhone and wait for the Wifi-indicator to show a connection before entering the house. Most of the time this happens very quickly or has already happened when waking the iphone.

I have looked at other solutions but came to the conclusion that app-based or ping-based solutions are not practical. :-/ They are draining the batteries and/or are hard to implement and maintain. But I am open to suggestions! Maybe I overlooked something.

Thanks for your nice comment, SiLeX! I am very happy to hear that. I am planning on doing a posting about openHab programming in general but need to find the time. Also I do not know if this forum is the right place for it.

There’s a lot of documentation floating around and of course this brilliant forum. But I found it unnecessarily hard to get going when I started migrating to openHab. On the one hand some limiting aspects of openHab can be frustrating for professional programmers. On the other hand as a newcomer some relatively modern aspects like lambdas can get overwhelming. Take this and add other aspects like confusions about what IDE to use etc. and there are suddenly a lot of questions and too many different answers. This is not a criticism, btw. - I love openHab. It’s just a fact and one reason why I wrote this posting.