Home » Room presence detection app for Android

Room presence detection app for Android

If you are into smart homes for some time already, then probably you encountered a situation, where it would be useful if your smart home system was aware of which person is in which room, or putting it simply – it had a room presence detection. Imagine a situation, where you enter the bedroom to the music of The Imperial March from Star Wars or get a personalized notification when you are sitting too long in the bathroom. How great would it be, right?

There are already some solutions for that, like setting up few ESP32 in different rooms, which will scan nearby BLE (Bluetooth Low Energy) devices looking for your phone or smart watch. Whichever detects it, then it sends proper message to your smart home system. This would require buying some additional hardware, as well as maintaining proper software on each of these devices.

But let’s do it actually the other way around. You may already have some devices at home which you could use as beacons and use your phone to scan for them. In this blog post I will explain some technical details how to do it. Starting from some theory and finishing on showing how to write an app for Android which will be our room presence detection app!

How it will work?

The idea is simple, we’ll need some BLE devices which will be our beacons placed in each room. They also need to broadcast some advertising packets. The second component is a custom app installed on the phone, which will scan in the background for those devices. Of course, the app could discover a few of our beacons at the same time. To distinguish in which room we are currently in, we will use RSSI (Received Signal Strength Indicator), which we get with the advertising data. We’ll assume that the stronger the signal, the more likely it is that this is the room associated with the beacon. So far not so complicated, right?

Commercial devices as beacons

You don’t necessarily need to buy special devices which will act as beacons. As a matter of fact, you may already have some at home which you can use. Some time ago I bought a few Xiaomi Mi Temperature & Humidity Monitor sensors for each room at my apartment. They have Bluetooth chips inside and what’s more, they are constantly advertising some data. So I thought, why would I buy additional beacons if I have them already! If you are not sure about having any BLE devices at home which are advertising, you can check it for example with nRF Connect app. It’s a simple tool to scan nearby devices. You can also use it to get their mac addresses (they will be needed later for our custom app).

Room presence detection app

Now let’s put together a simple app. It will detect our beacons and notify our smart home system in which room the phone is currently placed.

Before we jump into the code, few explanations:

  • As a way of communication with my smart home system I chose MQTT, it’s quite popular and easy to integrate. In our app, I’m using the HiveMQ MQTT Client library.
  • I’m using Hilt for dependency injection. If you are not familiar with it, then don’t worry. It’s quite friendly to use and you can learn more about it here.

Ok then, enough with the theory, let’s see the code! If you want to see the full source code already, you can find it here:

The user’s journey starts in the MainActivity. Its role is to request required runtime permissions for the Bluetooth and location. From the UI perspective, it has 2 buttons to start and stop room detecting.

Room presence detection app screenshot

After pressing Start button, underneath it starts another Android component – RoomDetectorService. It is a service running for the whole time. Since it’s a foreground service, you can see the notification in the status bar.

Room presence detection app notification

In both (the MainActivity and the notification) you can see the Stop button. The functionality is the same, it just stops the RoomDetectorService.

Background BLE scanning

Now let’s go through the most interesting part of the code, a starting point would be the RoomDetectorService.

@AndroidEntryPoint
class RoomDetectorService : Service() {

    @Inject
    lateinit var notifications: Notifications

    @Inject
    lateinit var roomDetector: RoomDetector

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()
        startForeground(1, notifications.buildNotification())
        roomDetector.start()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        roomDetector.onScanResults(intent.toBleScanResults())
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        roomDetector.stop()
        super.onDestroy()
    }
}

As you can see we are delegating most of the work to the RoomDetector class, where our main business logic is placed. However, RoomDetectorService is important to manage the lifecycle of RoomDetector. Here’s the code of RoomDetector class:

@Singleton
class RoomDetector @Inject constructor(
    private val bleClient: BleClient,
    private val mqttClient: MqttClient,
) {

    private val cachedScanResults = mutableMapOf<String, Int>()
    private var currentRoom = ""

    fun start() {
        mqttClient.connect()
        bleClient.startBackgroundScan()
    }

    fun onScanResults(scanResults: List<BleScanResult>) {
        scanResults.forEach { cachedScanResults[it.macAddress] = it.rssi }
        val room = cachedScanResults.maxByOrNull { it.value }
            ?.let { RoomsConfig.rooms[it.key] }
        if (room != null && room != currentRoom) {
            currentRoom = room
            mqttClient.publish(currentRoom)
        }
    }

    fun stop() {
        bleClient.stopBackgroundScan()
        mqttClient.disconnect()
    }
}

start() – We are connecting to the MqttClient and starting the BLE background scan (we’ll get back to that later for more technical details).

onScanResults() – It is called whenever there are new scan results. We update our cached scan results (it’s a map of MAC addresses associated to last known RSSI), then we find the max value of RSSI and map it to our room name, based on the MAC address. If the room has changed, then we publish a new value with our MQTT client.

stop() – We simply stop the background BLE scan and disconnect from the MQTT client.

So as you can see the business logic is quite simple. Now let’s check how the background BLE scan is actually done.

class BleClient @Inject constructor(
    @ApplicationContext private val context: Context,
    private val pendingIntentBuilder: PendingIntentBuilder,
) {

    private val bluetoothLeScanner =
        context.getSystemService<BluetoothManager>()!!.adapter.bluetoothLeScanner

    @SuppressLint("MissingPermission")
    fun startBackgroundScan() {
        bluetoothLeScanner.startScan(
            listOf(ScanFilter.Builder().setDeviceName("MJ_HT_V1").build()),
            ScanSettings.Builder()
                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .build(),
            pendingIntentBuilder.buildForService()
        )
    }

    @SuppressLint("MissingPermission")
    fun stopBackgroundScan() {
        bluetoothLeScanner.stopScan(pendingIntentBuilder.buildForService())
    }
}

The core part here is the BluetoothLeScanner.startScan() from Android framework. It takes 3 parameters:

  • ScanFilter – I use here a device name as filter, in my case those are mentioned earlier temperature sensors.
  • ScanSettings – We want to get callback whenever new advertising data is available from specific devices. That’s why we are using here CALLBACK_TYPE_ALL_MATCHES.
  • PendingIntent – It will call onStartCommand() in our RoomDetectorService whenever there are new scan results available.

And that would conclude the most interesting parts of the app from the code perspective. For the rest please check the repository on Github:

What’s next?

You should treat it as a base mechanism, which on top of, you can build your own solutions! In order to make this simple app work, you will need to fill in few details in the code, but you can find how to do it easily in README on Github.

Let’s not forget that we are dealing here with multiple BLE beacons, which can be placed quite close to each other and signals may overlap, then the logic for choosing the beacon just with the highest RSSI might be not enough and you may need to think about more sophisticated solution or about putting your beacons in different places. Depending on which devices you will use as beacons, you may need to also think about your own ScanFilter which you will use for BluetoothLeScanner.

To sum up, room presence detection is just kind of a sensor, providing us some data which can be used later for home automations. The most interesting part though, is what we can actually do with it. I have few ideas already how to use it, which I will describe in next posts, so make sure to follow my blog to not miss them!

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.