Repository: openhab/openhab-android (language: kotlin)

Issue:
Android Device Controls: subtitle
### Is your feature request related to a problem? Please describe.
Android Device Controls, subtitles missing. Opportunity to display useful Information like "item.name" missed.

### Describe the solution you'd like
Use` .setSubtitle() `to add additional Information like location or name of parent item. 

I added: 
`.setSubtitle(item.name)`
under line 319 and under line 326 in the file `ItemsControlsProviderService.kt` (path: \openhab-android-main\mobile\src\main\java\org\openhab\habdroid\background)

The results can be seen in the screenshots below.

### Describe alternatives you've considered
Showing the item location, instead of item name, would also be a good alternative.

### Additional context
**Current state (used paint to remove subtitles, as I use the app with my the implemented code now):**

<img src='https://user-images.githubusercontent.com/36895999/167680478-2dd204b7-4146-4614-85da-7dd7091a9bf6.png' width='300'>


**With implemented Code:**

<img src='https://user-images.githubusercontent.com/36895999/167680548-882d5bd0-a9b6-4b1d-b468-ca10ff9690b0.png' width='300'>




Discussion hints:
There aren't many properties that are connected to Items: https://github.com/openhab/openhab-android/blob/main/mobile/src/main/java/org/openhab/habdroid/model/Item.kt#L30-L43

IMO the item name is too technical to be displayed here.

Maybe display the item state, e.g. for temperature?
Temperature and other item states are usually displayed on the right side of the icon :)  

https://developer.android.com/guide/topics/ui/device-control
> IMO the item name is too technical to be displayed here.

Not sure. Think e.g. having 5 switches of some outlet relay plugs, which each have the default name of 'State' (or likewise 'Level' for roller shutters)... in that case, seeing the item name would be really helpful.
After all, we show the item name in the item picker as well...
>  After all, we show the item name in the item picker as well...

True, but the item picker isn't made for being used every day.

But I also see that it helps, so no veto from my side. @KingTomaHawk Do you want to open a PR?
@mueller-ma never done a PR till today, so please check if everything is done right :)
 
**achievement unlocked: create first PR**
TBH I'm not really happy with that change. The UI looked much cleaner before and for some users, e.g. for me, the item name doesn't add any information as the item label is already unique.

In the [documentation ](https://developer.android.com/guide/topics/ui/device-control#user-interface)the subtitle is the location of the item. We could do that, but I'm not sure how to get the location. For things I can set a location, but not for items....

The location could be set as [zone](https://developer.android.com/reference/android/service/controls/Control.StatefulBuilder#setZone(java.lang.CharSequence)). This makes it easier to find item when searching in the long list of items.

Maybe add a preference subscreen with a setting for the subtitle: Item name, location or nothing?
In this screen there could be an setting for https://developer.android.com/reference/android/service/controls/Control.StatefulBuilder#setAuthRequired(boolean)
Hmm.. I understand that. You said that it looked cleaner before, but I don't understand what exactly the problem is. Is the problem that some text doesn't fit (which I fully understand, that bothers me too) or that there is text at all?

But It seems like Android 13 Beta 2 broke something with Device Controls so I can't experiment with it for a while.
The "problem" for me is that the item name as subtitle adds redundant and technical information.

Pull request: Make subtitle in device controls configurable
Fixes #2904

Use the semantic model for getting the location: https://github.com/openhab/openhab-core/issues/2977#issuecomment-1134772074

Test command: ./gradlew :mobile:testFossBetaDebugUnitTest --tests "org.openhab.habdroid.model.ItemTest"

Fix patch:
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 65abf3cba2..a9d745735e 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -55,6 +55,7 @@ There are a number of strategies available to provide [secure remote access]({{b
 * [Send device information to openHAB](#send-device-information-to-openhab), like next alarm clock time or call state
 * [Supports wall mounted tablets](#permanent-deployment)
 * [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) action plugin included
+* [Android Quick Access Device Controls](#quick-access-device-controls)
 
 ### Permanent Deployment
 
@@ -313,6 +314,11 @@ In case of an error the plugin returns an error code.
 | 11         | The app couldn't establish a connection                                                                                                                                                                    |
 | 1000+      | A connection was established, but an error occurred. The error code is 1000 + the HTTP code, e.g. 1401 means [Unauthenticated](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors). |
 
+### Quick Access Device Controls
+
+openHAB supports the device controls introduced in Android 11: https://www.android.com/intl/en_US/android-11/#a11-device-controls-article
+When using a [semantic model](https://www.openhab.org/docs/tutorial/model.html#semantic-model) the location and/or equipment name can be shown in the tiles.
+
 ## Multi server support
 
 When adding multiple servers to the app, there's always a primary and an active one.
diff --git a/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt b/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt
index 7d0ef1215e..e3da66a389 100644
--- a/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt
+++ b/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt
@@ -50,8 +50,10 @@ import org.openhab.habdroid.model.ParsedState
 import org.openhab.habdroid.model.ServerConfiguration
 import org.openhab.habdroid.model.toParsedState
 import org.openhab.habdroid.ui.MainActivity
+import org.openhab.habdroid.util.DeviceControlSubtitleMode
 import org.openhab.habdroid.util.HttpClient
 import org.openhab.habdroid.util.ItemClient
+import org.openhab.habdroid.util.getDeviceControlSubtitle
 import org.openhab.habdroid.util.getPrefs
 import org.openhab.habdroid.util.getPrimaryServerId
 import org.openhab.habdroid.util.getSecretPrefs
@@ -95,6 +97,7 @@ class ItemsControlsProviderService : ControlsProviderService() {
         val primaryServerName = ServerConfiguration.load(getPrefs(), getSecretPrefs(), getPrefs().getPrimaryServerId())
             ?.name
             .orDefaultIfEmpty(getString(R.string.app_name))
+        val subtitleMode = getPrefs().getDeviceControlSubtitle(applicationContext)
         val job = GlobalScope.launch {
             ConnectionFactory.waitForInitialization()
             val connection = ConnectionFactory.primaryUsableConnection?.connection
@@ -103,21 +106,16 @@ class ItemsControlsProviderService : ControlsProviderService() {
                 controls.done = true
                 return@launch
             }
-            val items = try {
-                ItemClient.loadItems(connection)
+            val allItems = try {
+                ItemClient.loadItems(connection) ?: return@launch
             } catch (e: HttpClient.HttpException) {
                 Log.e(TAG, "Could not load items", e)
                 controls.done = true
                 return@launch
-            }
-            if (items == null) {
-                Log.e(TAG, "Could not load items")
-                controls.done = true
-                return@launch
-            }
-            items.forEach { item ->
-                maybeCreateControl(item, primaryServerName, false)?.let { control -> controls.add(control) }
-            }
+            }.associateBy { item -> item.name }
+
+            allItems.mapNotNull { maybeCreateControl(it.value, allItems, primaryServerName, subtitleMode, false) }
+                .forEach { control -> controls.add(control) }
             controls.done = true
         }
         controls.onCancel = {
@@ -129,10 +127,10 @@ class ItemsControlsProviderService : ControlsProviderService() {
     override fun createPublisherFor(itemNames: MutableList<String>): Flow.Publisher<Control> {
         val publisher = SimplePublisher<Control>()
         var eventStream: EventSource? = null
-        val items = mutableMapOf<String, Item>()
         val primaryServerName = ServerConfiguration.load(getPrefs(), getSecretPrefs(), getPrefs().getPrimaryServerId())
             ?.name
             .orDefaultIfEmpty(getString(R.string.app_name))
+        val subtitleMode = getPrefs().getDeviceControlSubtitle(applicationContext)
         val job = launchWithConnection { connection ->
             if (connection == null) {
                 Log.e(TAG, "Got no connection for loading items")
@@ -140,26 +138,24 @@ class ItemsControlsProviderService : ControlsProviderService() {
                 return@launchWithConnection
             }
 
-            itemNames.forEach { itemName ->
-                try {
-                    ItemClient.loadItem(connection, itemName)?.let { item ->
-                        maybeCreateControl(item, primaryServerName, true)?.let { control ->
-                            items[item.name] = item
-                            publisher.add(control)
-                        }
-                    }
-                } catch (e: HttpClient.HttpException) {
-                    Log.e(TAG, "Could not load item $itemName", e)
-                }
-            }
+            val allItems = try {
+                ItemClient.loadItems(connection) ?: return@launchWithConnection
+            } catch (e: HttpClient.HttpException) {
+                Log.e(TAG, "Could not load items", e)
+                return@launchWithConnection
+            }.associateBy { item -> item.name }
+
+            allItems.filterKeys { itemName -> itemName in itemNames }
+                .mapNotNull { maybeCreateControl(it.value, allItems, primaryServerName, subtitleMode, true) }
+                .forEach { control -> publisher.add(control) }
 
             eventStream = connection.httpClient.makeSse(
                 // Support for both the "openhab" and the older "smarthome" root topic by using a wildcard
                 connection.httpClient.buildUrl("rest/events?topics=*/items/*/statechanged"),
                 StateChangeListener { itemName, state ->
-                    val item = items[itemName] ?: return@StateChangeListener
+                    val item = allItems[itemName] ?: return@StateChangeListener
                     val newItem = item.copy(state = state)
-                    maybeCreateControl(newItem, primaryServerName, true)?.let { control ->
+                    maybeCreateControl(newItem, allItems, primaryServerName, subtitleMode, true)?.let { control ->
                         publisher.add(control)
                     }
                 }
@@ -201,6 +197,27 @@ class ItemsControlsProviderService : ControlsProviderService() {
         }
     }
 
+    private fun getItemTagLabel(item: Item, allItems: Map<String, Item>, type: Item.Tag): String? {
+        val groups = item.groupNames.mapNotNull { groupName -> allItems[groupName] }
+        // First check if any of the groups is the requested type
+        groups.forEach { group ->
+            if (group.tags.any { tag -> tag == type }) {
+                return group.label
+            }
+            val tagByType = group.tags.firstOrNull { tag -> tag.parent == type }
+            if (tagByType != null) {
+                return if (group.label.isNullOrBlank() && tagByType.labelResId != null) {
+                    getString(tagByType.labelResId)
+                } else {
+                    group.label
+                }
+            }
+        }
+
+        // If none of the groups is location or equipment, recursively check parent groups
+        return groups.firstNotNullOfOrNull { group -> getItemTagLabel(group, allItems, type) }
+    }
+
     private fun getDeviceType(item: Item) =
         when (item.category?.lowercase()) {
             "screen", "soundvolume", "receiver" -> DeviceTypes.TYPE_TV
@@ -303,7 +320,13 @@ class ItemsControlsProviderService : ControlsProviderService() {
         )
     }
 
-    private fun maybeCreateControl(item: Item, serverName: String, stateful: Boolean): Control? {
+    private fun maybeCreateControl(
+        item: Item,
+        allItems: Map<String, Item>,
+        serverName: String,
+        subtitleMode: DeviceControlSubtitleMode,
+        stateful: Boolean
+    ): Control? {
         if (item.label.isNullOrEmpty() || item.readOnly) return null
         val controlTemplate = when (item.type) {
             Item.Type.Switch -> ToggleTemplate(
@@ -323,22 +346,36 @@ class ItemsControlsProviderService : ControlsProviderService() {
             else -> return null
         }
 
+        val location = getItemTagLabel(item, allItems, Item.Tag.Location).orEmpty()
+        val equipment = getItemTagLabel(item, allItems, Item.Tag.Equipment).orEmpty()
+
+        val subtitle = when (subtitleMode) {
+            DeviceControlSubtitleMode.LOCATION -> location
+            DeviceControlSubtitleMode.EQUIPMENT -> equipment
+            DeviceControlSubtitleMode.LOCATION_AND_EQUIPMENT -> "$location $equipment"
+            DeviceControlSubtitleMode.ITEM_NAME -> item.name
+        }
+
+        val zone = if (subtitleMode == DeviceControlSubtitleMode.ITEM_NAME) {
+            item.name.first().toString()
+        } else {
+            location
+        }
+
+        val statefulControl = Control.StatefulBuilder(item.name, mainActivityPendingIntent)
+            .setTitle(item.label)
+            .setSubtitle(subtitle)
+            .setZone(zone)
+            .setStructure(serverName)
+            .setDeviceType(getDeviceType(item))
+            .setControlTemplate(controlTemplate)
+            .setStatus(Control.STATUS_OK)
+            .build()
+
         return if (stateful) {
-            Control.StatefulBuilder(item.name, mainActivityPendingIntent)
-                .setTitle(item.label)
-                .setSubtitle(item.name)
-                .setStructure(serverName)
-                .setDeviceType(getDeviceType(item))
-                .setControlTemplate(controlTemplate)
-                .setStatus(Control.STATUS_OK)
-                .build()
+            statefulControl
         } else {
-            Control.StatelessBuilder(item.name, mainActivityPendingIntent)
-                .setTitle(item.label)
-                .setSubtitle(item.name)
-                .setStructure(serverName)
-                .setDeviceType(getDeviceType(item))
-                .build()
+            Control.StatelessBuilder(statefulControl).build()
         }
     }
 
diff --git a/mobile/src/main/java/org/openhab/habdroid/model/Item.kt b/mobile/src/main/java/org/openhab/habdroid/model/Item.kt
index 6439cc8b5f..c0cc987668 100644
--- a/mobile/src/main/java/org/openhab/habdroid/model/Item.kt
+++ b/mobile/src/main/java/org/openhab/habdroid/model/Item.kt
@@ -18,6 +18,7 @@ import java.util.Locale
 import kotlinx.parcelize.Parcelize
 import org.json.JSONException
 import org.json.JSONObject
+import org.openhab.habdroid.R
 import org.openhab.habdroid.util.forEach
 import org.openhab.habdroid.util.map
 import org.openhab.habdroid.util.mapString
@@ -38,6 +39,7 @@ data class Item internal constructor(
     val options: List<LabeledValue>?,
     val state: ParsedState?,
     val tags: List<Tag>,
+    val groupNames: List<String>,
     val minimum: Double?,
     val maximum: Double?,
     val step: Double?,
@@ -60,149 +62,155 @@ data class Item internal constructor(
     }
 
     @Suppress("unused")
-    enum class Tag {
+    enum class Tag(val parent: Tag?, val labelResId: Int?) {
         // Legacy tags from openHAB 2
-        ContactSensor,
-        HeatingCoolingMode,
-        TargetTemperature,
+        ContactSensor(null, null),
+        HeatingCoolingMode(null, null),
+        TargetTemperature(null, null),
+
+        // Fallback
+        Unknown(null, null),
 
         // Semantic types
-        Equipment,
-        Location,
-        Point,
-        Property,
+        Equipment(null, null),
+        Location(null, null),
+        Point(null, null),
+        Property(null, null),
 
-        // Semantic tags
-        Alarm,
-        AlarmSystem,
-        Apartment,
-        Attic,
-        BackDoor,
-        Basement,
-        Bathroom,
-        Battery,
-        Bedroom,
-        Blinds,
-        Boiler,
-        BoilerRoom,
-        Building,
-        CO,
-        CO2,
-        Camera,
-        Car,
-        Carport,
-        CeilingFan,
-        Cellar,
-        CellarDoor,
-        CleaningRobot,
-        ColorTemperature,
-        Control,
-        Corridor,
-        Current,
-        DiningRoom,
-        Dishwasher,
-        Door,
-        Doorbell,
-        Driveway,
-        Dryer,
-        Duration,
-        Energy,
-        Entry,
-        FamilyRoom,
-        Fan,
-        FirstFloor,
-        Floor,
-        Freezer,
-        Frequency,
-        FrontDoor,
-        Garage,
-        GarageDoor,
-        Garden,
-        Gas,
-        Gate,
-        GroundFloor,
-        GuestRoom,
-        HVAC,
-        House,
-        Humidity,
-        Indoor,
-        InnerDoor,
-        Inverter,
-        Kitchen,
-        KitchenHood,
-        LaundryRoom,
-        LawnMower,
-        Level,
-        Light,
-        LightStripe,
-        Lightbulb,
-        LivingRoom,
-        Lock,
-        LowBattery,
-        Measurement,
-        MotionDetector,
-        NetworkAppliance,
-        Noise,
-        Office,
-        Oil,
-        OpenLevel,
-        OpenState,
-        Opening,
-        Outdoor,
-        Oven,
-        Patio,
-        Porch,
-        Power,
-        PowerOutlet,
-        Presence,
-        Pressure,
-        Projector,
-        Pump,
-        RadiatorControl,
-        Rain,
-        Receiver,
-        Refrigerator,
-        RemoteControl,
-        Room,
-        Screen,
-        SecondFloor,
-        Sensor,
-        Setpoint,
-        Shed,
-        SideDoor,
-        Siren,
-        Smartphone,
-        Smoke,
-        SmokeDetector,
-        SoundVolume,
-        Speaker,
-        Status,
-        SummerHouse,
-        Switch,
-        Tampered,
-        Television,
-        Temperature,
-        Terrace,
-        ThirdFloor,
-        Tilt,
-        Timestamp,
-        Ultraviolet,
-        Valve,
-        Veranda,
-        Vibration,
-        VoiceAssistant,
-        Voltage,
-        WallSwitch,
-        WashingMachine,
-        Water,
-        WeatherService,
-        WebService,
-        WhiteGood,
-        Wind,
-        Window,
+        // Semantic tags: point types
+        Alarm(Point, null),
+        Control(Point, null),
+        Measurement(Point, null),
+        LowBattery(Point, null),
+        OpenLevel(Point, null),
+        OpenState(Point, null),
+        Setpoint(Point, null),
+        Status(Point, null),
+        Switch(Point, null),
+        Tampered(Point, null),
+        Tilt(Point, null),
 
-        // Fallback
-        Unknown
+        // Semantic tags: measurement types
+        CO(Measurement, null),
+        CO2(Measurement, null),
+        ColorTemperature(Measurement, null),
+        Current(Measurement, null),
+        Duration(Measurement, null),
+        Energy(Measurement, null),
+        Frequency(Measurement, null),
+        Gas(Measurement, null),
+        Humidity(Measurement, null),
+        Level(Measurement, null),
+        Light(Measurement, null),
+        Noise(Measurement, null),
+        Oil(Measurement, null),
+        Opening(Measurement, null),
+        Power(Measurement, null),
+        Presence(Measurement, null),
+        Pressure(Measurement, null),
+        Rain(Measurement, null),
+        Smoke(Measurement, null),
+        SoundVolume(Measurement, null),
+        Temperature(Measurement, null),
+        Timestamp(Measurement, null),
+        Ultraviolet(Measurement, null),
+        Vibration(Measurement, null),
+        Voltage(Measurement, null),
+        Water(Measurement, null),
+        Wind(Measurement, null),
+
+        // Semantic tags: location
+        Apartment(Location, R.string.item_tag_location_apartment),
+        Attic(Location, R.string.item_tag_location_attic),
+        Basement(Location, R.string.item_tag_location_basement),
+        Bathroom(Location, R.string.item_tag_location_bathroom),
+        Bedroom(Location, R.string.item_tag_location_bedroom),
+        BoilerRoom(Location, R.string.item_tag_location_boilerroom),
+        Building(Location, R.string.item_tag_location_building),
+        Carport(Location, R.string.item_tag_location_carport),
+        Cellar(Location, R.string.item_tag_location_cellar),
+        Corridor(Location, R.string.item_tag_location_corridor),
+        DiningRoom(Location, R.string.item_tag_location_diningroom),
+        Driveway(Location, R.string.item_tag_location_driveway),
+        Entry(Location, R.string.item_tag_location_entry),
+        FamilyRoom(Location, R.string.item_tag_location_familyroom),
+        FirstFloor(Location, R.string.item_tag_location_firstfloor),
+        Floor(Location, R.string.item_tag_location_floor),
+        Garage(Location, R.string.item_tag_location_garage),
+        Garden(Location, R.string.item_tag_location_garden),
+        GroundFloor(Location, R.string.item_tag_location_groundfloor),
+        GuestRoom(Location, R.string.item_tag_location_guestroom),
+        House(Location, R.string.item_tag_location_house),
+        Indoor(Location, R.string.item_tag_location_indoor),
+        Kitchen(Location, R.string.item_tag_location_kitchen),
+        LaundryRoom(Location, R.string.item_tag_location_laundryroom),
+        LivingRoom(Location, R.string.item_tag_location_livingroom),
+        Office(Location, R.string.item_tag_location_office),
+        Outdoor(Location, R.string.item_tag_location_outdoor),
+        Patio(Location, R.string.item_tag_location_patio),
+        Porch(Location, R.string.item_tag_location_porch),
+        Room(Location, R.string.item_tag_location_room),
+        SecondFloor(Location, R.string.item_tag_location_secondfloor),
+        Shed(Location, R.string.item_tag_location_shed),
+        SummerHouse(Location, R.string.item_tag_location_summerhouse),
+        Terrace(Location, R.string.item_tag_location_terrace),
+        ThirdFloor(Location, R.string.item_tag_location_thirdfloor),
+        Veranda(Location, R.string.item_tag_location_veranda),
+
+        // Semantic tags: equipment
+        AlarmSystem(Equipment, R.string.item_tag_equipment_alarmsystem),
+        BackDoor(Equipment, R.string.item_tag_equipment_backdoor),
+        Battery(Equipment, R.string.item_tag_equipment_battery),
+        Blinds(Equipment, R.string.item_tag_equipment_blinds),
+        Boiler(Equipment, R.string.item_tag_equipment_boiler),
+        Camera(Equipment, R.string.item_tag_equipment_camera),
+        Car(Equipment, R.string.item_tag_equipment_car),
+        CeilingFan(Equipment, R.string.item_tag_equipment_ceilingfan),
+        CellarDoor(Equipment, R.string.item_tag_equipment_cellardoor),
+        CleaningRobot(Equipment, R.string.item_tag_equipment_cleaningrobot),
+        Dishwasher(Equipment, R.string.item_tag_equipment_dishwasher),
+        Door(Equipment, R.string.item_tag_equipment_door),
+        Doorbell(Equipment, R.string.item_tag_equipment_doorbell),
+        Dryer(Equipment, R.string.item_tag_equipment_dryer),
+        Fan(Equipment, R.string.item_tag_equipment_fan),
+        Freezer(Equipment, R.string.item_tag_equipment_freezer),
+        FrontDoor(Equipment, R.string.item_tag_equipment_frontdoor),
+        GarageDoor(Equipment, R.string.item_tag_equipment_garagedoor),
+        Gate(Equipment, R.string.item_tag_equipment_gate),
+        HVAC(Equipment, R.string.item_tag_equipment_hvac),
+        InnerDoor(Equipment, R.string.item_tag_equipment_innerdoor),
+        Inverter(Equipment, R.string.item_tag_equipment_inverter),
+        KitchenHood(Equipment, R.string.item_tag_equipment_kitchenhood),
+        LawnMower(Equipment, R.string.item_tag_equipment_lawnmower),
+        Lightbulb(Equipment, R.string.item_tag_equipment_lightbulb),
+        LightStripe(Equipment, R.string.item_tag_equipment_lightstripe),
+        Lock(Equipment, R.string.item_tag_equipment_lock),
+        MotionDetector(Equipment, R.string.item_tag_equipment_motiondetector),
+        NetworkAppliance(Equipment, R.string.item_tag_equipment_networkappliance),
+        Oven(Equipment, R.string.item_tag_equipment_oven),
+        PowerOutlet(Equipment, R.string.item_tag_equipment_poweroutlet),
+        Projector(Equipment, R.string.item_tag_equipment_projector),
+        Pump(Equipment, R.string.item_tag_equipment_pump),
+        RadiatorControl(Equipment, R.string.item_tag_equipment_radiatorcontrol),
+        Receiver(Equipment, R.string.item_tag_equipment_receiver),
+        Refrigerator(Equipment, R.string.item_tag_equipment_refrigerator),
+        RemoteControl(Equipment, R.string.item_tag_equipment_remotecontrol),
+        Screen(Equipment, R.string.item_tag_equipment_screen),
+        Sensor(Equipment, R.string.item_tag_equipment_sensor),
+        SideDoor(Equipment, R.string.item_tag_equipment_sidedoor),
+        Siren(Equipment, R.string.item_tag_equipment_siren),
+        Smartphone(Equipment, R.string.item_tag_equipment_smartphone),
+        SmokeDetector(Equipment, R.string.item_tag_equipment_smokedetector),
+        Speaker(Equipment, R.string.item_tag_equipment_speaker),
+        Television(Equipment, R.string.item_tag_equipment_television),
+        Valve(Equipment, R.string.item_tag_equipment_valve),
+        VoiceAssistant(Equipment, R.string.item_tag_equipment_voiceassistant),
+        WallSwitch(Equipment, R.string.item_tag_equipment_wallswitch),
+        WashingMachine(Equipment, R.string.item_tag_equipment_washingmachine),
+        WeatherService(Equipment, R.string.item_tag_equipment_weatherservice),
+        WebService(Equipment, R.string.item_tag_equipment_webservice),
+        WhiteGood(Equipment, R.string.item_tag_equipment_whitegood),
+        Window(Equipment, R.string.item_tag_equipment_window)
     }
 
     fun isOfTypeOrGroupType(type: Type): Boolean {
@@ -254,20 +262,21 @@ fun Node.toItem(): Item? {
     }
 
     return Item(
-        finalName,
-        finalName.trim(),
-        null,
-        type,
-        groupType,
-        link,
-        false,
-        emptyList(),
-        null,
-        state.toParsedState(),
-        emptyList(),
-        null,
-        null,
-        null
+        name = finalName,
+        label = finalName.trim(),
+        category = null,
+        type = type,
+        groupType = groupType,
+        link = link,
+        readOnly = false,
+        members = emptyList(),
+        options = null,
+        state = state.toParsedState(),
+        tags = emptyList(),
+        groupNames = emptyList(),
+        minimum = null,
+        maximum = null,
+        step = null
     )
 }
 
@@ -310,6 +319,12 @@ fun JSONObject.toItem(): Item {
         emptyList()
     }
 
+    val groupNames = if (has("groupNames")) {
+        getJSONArray("groupNames").mapString { it }
+    } else {
+        emptyList()
+    }
+
     return Item(
         name,
         optStringOrNull("label")?.trim(),
@@ -322,6 +337,7 @@ fun JSONObject.toItem(): Item {
         options,
         state.toParsedState(numberPattern),
         tags,
+        groupNames,
         stateDescription?.optDoubleOrNull("minimum"),
         stateDescription?.optDoubleOrNull("maximum"),
         stateDescription?.optDoubleOrNull("step")
diff --git a/mobile/src/main/java/org/openhab/habdroid/util/PrefExtensions.kt b/mobile/src/main/java/org/openhab/habdroid/util/PrefExtensions.kt
index 69673d069e..6d25d6570f 100644
--- a/mobile/src/main/java/org/openhab/habdroid/util/PrefExtensions.kt
+++ b/mobile/src/main/java/org/openhab/habdroid/util/PrefExtensions.kt
@@ -188,6 +188,23 @@ fun SharedPreferences.isItemUpdatePrefEnabled(key: String) = getString(key, null
 
 fun SharedPreferences.isEventListenerEnabled() = getBoolean(PrefKeys.SEND_DEVICE_INFO_FOREGROUND_SERVICE, false)
 
+enum class DeviceControlSubtitleMode {
+    LOCATION,
+    EQUIPMENT,
+    LOCATION_AND_EQUIPMENT,
+    ITEM_NAME
+}
+
+fun SharedPreferences.getDeviceControlSubtitle(context: Context): DeviceControlSubtitleMode {
+    return when (getStringOrNull(PrefKeys.DEVICE_CONTROL_SUBTITLE)) {
+        context.getString(R.string.device_control_subtitle_equipment_value) -> DeviceControlSubtitleMode.EQUIPMENT
+        context.getString(R.string.device_control_subtitle_location_equipment_value) ->
+            DeviceControlSubtitleMode.LOCATION_AND_EQUIPMENT
+        context.getString(R.string.device_control_subtitle_item_name_value) -> DeviceControlSubtitleMode.ITEM_NAME
+        else -> DeviceControlSubtitleMode.LOCATION
+    }
+}
+
 /**
  * Returns vibration pattern for notifications that can be passed to
  * [}][androidx.core.app.NotificationCompat.Builder.setVibrate]
diff --git a/mobile/src/main/java/org/openhab/habdroid/util/PrefKeys.kt b/mobile/src/main/java/org/openhab/habdroid/util/PrefKeys.kt
index 21a38d47b8..5a94a63caa 100644
--- a/mobile/src/main/java/org/openhab/habdroid/util/PrefKeys.kt
+++ b/mobile/src/main/java/org/openhab/habdroid/util/PrefKeys.kt
@@ -72,6 +72,7 @@ object PrefKeys {
     const val DEV_ID_PREFIX_BG_TASKS = "device_identifier_prefix_background_tasks"
     const val SCREEN_LOCK = "screen_lock"
     const val TASKER_PLUGIN_ENABLED = "taskerPlugin"
+    const val DEVICE_CONTROL_SUBTITLE = "device_control_subtitle"
     const val DATA_SAVER = "data_saver"
     const val CRASH_REPORTING = "crash_reporting"
     const val DEBUG_MESSAGES = "default_openhab_debug_messages"
diff --git a/mobile/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml
index 43f57fae84..e135627ee1 100644
--- a/mobile/src/main/res/values/arrays.xml
+++ b/mobile/src/main/res/values/arrays.xml
@@ -130,6 +130,20 @@
         <item>@string/settings_screen_lock_on_value</item>
     </string-array>
 
+    <string-array name="device_control_subtitle">
+        <item>@string/device_control_subtitle_location</item>
+        <item>@string/device_control_subtitle_equipment</item>
+        <item>@string/device_control_subtitle_location_equipment</item>
+        <item>@string/device_control_subtitle_item_name</item>
+    </string-array>
+
+    <string-array name="device_control_subtitle_values">
+        <item>@string/device_control_subtitle_location_value</item>
+        <item>@string/device_control_subtitle_equipment_value</item>
+        <item>@string/device_control_subtitle_location_equipment_value</item>
+        <item>@string/device_control_subtitle_item_name_value</item>
+    </string-array>
+
     <array name="accentColors">
         <item>@color/openhab_orange</item>
         <item>@color/indigo_500</item>
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index b82b1b837f..3835deaa5c 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -130,6 +130,15 @@
     <string name="settings_multi_server_wifi_ssid_location_off">Unable to read Wi-Fi name: Location is off</string>
     <string name="setting_crash_reporting_title">Crash reporting</string>
     <string name="setting_crash_reporting_summary">Send crash reports to support the development of the app</string>
+    <string name="device_control_subtitle_location">Location</string>
+    <string name="device_control_subtitle_equipment">Equipment</string>
+    <string name="device_control_subtitle_location_equipment">Location and equipment</string>
+    <string name="device_control_subtitle_item_name">Item name</string>
+    <string name="device_control_subtitle_location_value" translatable="false">location</string>
+    <string name="device_control_subtitle_equipment_value" translatable="false">equipment</string>
+    <string name="device_control_subtitle_location_equipment_value" translatable="false">location_equipment</string>
+    <string name="device_control_subtitle_item_name_value" translatable="false">item_name</string>
+    <string name="device_control_subtitle">Device control subtitle</string>
 
     <!-- App messages strings -->
     <string name="title_voice_widget">Voice commands</string>
@@ -547,6 +556,7 @@
     <string name="widget_show_state">Append current state to name</string>
     <string name="widget_show_state_summary">The state is updated on the schedule that can be set in the settings under \"Send device information to server\"</string>
     <string name="widget_no_command">Don\'t send a command</string>
+    <string name="error_getting_state">Error loading state</string>
     <string name="item_update_success_message_generic">%1$s has been set to %2$s</string>
     <string name="item_update_success_message_on">%s has been turned on</string>
     <string name="item_update_success_message_off">%s has been turned off</string>
@@ -724,5 +734,95 @@
     <string name="tile_icon_lamp_floor_value" translatable="false">lamp_floor</string>
     <string name="tile_icon_lamp_bedside_value" translatable="false">lamp_bedside</string>
     <string name="tile_icon_lamp_outdoor_value" translatable="false">lamp_outdoor</string>
-    <string name="error_getting_state">Error loading state</string>
+
+    <!-- Item tag type labels -->
+    <string name="item_tag_location_apartment">Apartment</string>
+    <string name="item_tag_location_attic">Attic</string>
+    <string name="item_tag_location_basement">Basement</string>
+    <string name="item_tag_location_bathroom">Bathroom</string>
+    <string name="item_tag_location_bedroom">Bedroom</string>
+    <string name="item_tag_location_boilerroom">Boiler room</string>
+    <string name="item_tag_location_building">Building</string>
+    <string name="item_tag_location_carport">Carport</string>
+    <string name="item_tag_location_cellar">Cellar</string>
+    <string name="item_tag_location_corridor">Corridor</string>
+    <string name="item_tag_location_diningroom">Dining room</string>
+    <string name="item_tag_location_driveway">Driveway</string>
+    <string name="item_tag_location_entry">Entry</string>
+    <string name="item_tag_location_familyroom">Family room</string>
+    <string name="item_tag_location_firstfloor">First floor</string>
+    <string name="item_tag_location_floor">Floor</string>
+    <string name="item_tag_location_garage">Garage</string>
+    <string name="item_tag_location_garden">Garden</string>
+    <string name="item_tag_location_groundfloor">Ground floor</string>
+    <string name="item_tag_location_guestroom">Guest room</string>
+    <string name="item_tag_location_house">House</string>
+    <string name="item_tag_location_indoor">Indoor</string>
+    <string name="item_tag_location_kitchen">Kitchen</string>
+    <string name="item_tag_location_laundryroom">Laundry room</string>
+    <string name="item_tag_location_livingroom">Living room</string>
+    <string name="item_tag_location_office">Office</string>
+    <string name="item_tag_location_outdoor">Outdoor</string>
+    <string name="item_tag_location_patio">Patio</string>
+    <string name="item_tag_location_porch">Porch</string>
+    <string name="item_tag_location_room">Room</string>
+    <string name="item_tag_location_secondfloor">Second floor</string>
+    <string name="item_tag_location_shed">Shed</string>
+    <string name="item_tag_location_summerhouse">Summer house</string>
+    <string name="item_tag_location_terrace">Terrace</string>
+    <string name="item_tag_location_thirdfloor">Third floor</string>
+    <string name="item_tag_location_veranda">Veranda</string>
+    <string name="item_tag_equipment_alarmsystem">Alarm system</string>
+    <string name="item_tag_equipment_backdoor">Back door</string>
+    <string name="item_tag_equipment_battery">Battery</string>
+    <string name="item_tag_equipment_blinds">Blinds</string>
+    <string name="item_tag_equipment_boiler">Boiler</string>
+    <string name="item_tag_equipment_camera">Camera</string>
+    <string name="item_tag_equipment_car">Car</string>
+    <string name="item_tag_equipment_ceilingfan">CeilingFan</string>
+    <string name="item_tag_equipment_cellardoor">Cellar door</string>
+    <string name="item_tag_equipment_cleaningrobot">Cleaning robot</string>
+    <string name="item_tag_equipment_dishwasher">Dishwasher</string>
+    <string name="item_tag_equipment_door">Door</string>
+    <string name="item_tag_equipment_doorbell">Doorbell</string>
+    <string name="item_tag_equipment_dryer">Dryer</string>
+    <string name="item_tag_equipment_fan">Fan</string>
+    <string name="item_tag_equipment_freezer">Freezer</string>
+    <string name="item_tag_equipment_frontdoor">Front door</string>
+    <string name="item_tag_equipment_garagedoor">Garage door</string>
+    <string name="item_tag_equipment_gate">Gate</string>
+    <string name="item_tag_equipment_hvac">HVAC</string>
+    <string name="item_tag_equipment_innerdoor">Inner door</string>
+    <string name="item_tag_equipment_inverter">Inverter</string>
+    <string name="item_tag_equipment_kitchenhood">Kitchen hood</string>
+    <string name="item_tag_equipment_lawnmower">Lawn mower</string>
+    <string name="item_tag_equipment_lightbulb">Lightbulb</string>
+    <string name="item_tag_equipment_lightstripe">LightStripe</string>
+    <string name="item_tag_equipment_lock">Lock</string>
+    <string name="item_tag_equipment_motiondetector">Motion detector</string>
+    <string name="item_tag_equipment_networkappliance">Network appliance</string>
+    <string name="item_tag_equipment_oven">Oven</string>
+    <string name="item_tag_equipment_poweroutlet">Power outlet</string>
+    <string name="item_tag_equipment_projector">Projector</string>
+    <string name="item_tag_equipment_pump">Pump</string>
+    <string name="item_tag_equipment_radiatorcontrol">Radiator control</string>
+    <string name="item_tag_equipment_receiver">Receiver</string>
+    <string name="item_tag_equipment_refrigerator">Refrigerator</string>
+    <string name="item_tag_equipment_remotecontrol">Remote control</string>
+    <string name="item_tag_equipment_screen">Screen</string>
+    <string name="item_tag_equipment_sensor">Sensor</string>
+    <string name="item_tag_equipment_sidedoor">Side door</string>
+    <string name="item_tag_equipment_siren">Siren</string>
+    <string name="item_tag_equipment_smartphone">Smartphone</string>
+    <string name="item_tag_equipment_smokedetector">Smoke detector</string>
+    <string name="item_tag_equipment_speaker">Speaker</string>
+    <string name="item_tag_equipment_television">Television</string>
+    <string name="item_tag_equipment_valve">Valve</string>
+    <string name="item_tag_equipment_voiceassistant">Voice assistant</string>
+    <string name="item_tag_equipment_wallswitch">Wall switch</string>
+    <string name="item_tag_equipment_washingmachine">Washing machine</string>
+    <string name="item_tag_equipment_weatherservice">Weather service</string>
+    <string name="item_tag_equipment_webservice">Web service</string>
+    <string name="item_tag_equipment_whitegood">White good</string>
+    <string name="item_tag_equipment_window">Window</string>
 </resources>
diff --git a/mobile/src/main/res/xml/preferences.xml b/mobile/src/main/res/xml/preferences.xml
index c14d8c74fc..e051b82def 100644
--- a/mobile/src/main/res/xml/preferences.xml
+++ b/mobile/src/main/res/xml/preferences.xml
@@ -138,6 +138,13 @@
             android:entries="@array/screen_lock"
             android:entryValues="@array/screen_lock_values"
             android:icon="@drawable/ic_lock_outline_grey_24dp" />
+        <ListPreference
+            android:key="device_control_subtitle"
+            android:title="@string/device_control_subtitle"
+            android:defaultValue="@string/device_control_subtitle_location_value"
+            app:useSimpleSummaryProvider="true"
+            android:entries="@array/device_control_subtitle"
+            android:entryValues="@array/device_control_subtitle_values"/>
         <SwitchPreference
             android:defaultValue="false"
             android:key="taskerPlugin"


Test patch:
diff --git a/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt b/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt
index d4cff1b4e8..61df39b2ec 100644
--- a/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt
+++ b/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt
@@ -153,6 +153,16 @@ class ItemTest {
         assertNotEquals(sut1a.hashCode(), sut2a.hashCode())
     }
 
+    @Test
+    fun tagsHaveLabel() {
+        val mustHaveLabel = listOf(Item.Tag.Location, Item.Tag.Equipment)
+        Item.Tag.values()
+            .filter { tag -> tag.parent in mustHaveLabel }
+            .forEach { tag ->
+                assertNotNull(tag.labelResId)
+            }
+    }
+
     companion object {
         private val itemAsJsonObjectWithMembers = JSONObject(
             """