Matter: Create a virtual device and make an open source contribution


Objective

Learn how to create a Matter virtual device, which you can control using the SmartThings app.

Also, know how to contribute your code to Matter open source.

Overview

Matter is an open-source connectivity standard for smart home and Internet of Things (IoT) devices. It is a secure, reliable, and seamless cross-platform protocol for connecting compatible devices and systems with one another.

SmartThings provides the Matter virtual device app and SmartThings Home APIs to help you quickly develop Matter devices and use the SmartThings ecosystem without needing to build your own IoT ecosystem.

You can use SmartThings Home APIs to onboard, control, remove, and share all Matter devices when building your application. Other IoT ecosystems can use the Matter devices onboarded on your IoT app through the Multi-Admin function.

For detailed information, see Matter in SmartThings Developers.

Set up your environment

You will need the following:

  • Host PC running on Windows 10 (or higher) or Ubuntu 20.04 (x64)
  • Android Studio (latest version recommended)
  • Java SE Development Kit (JDK) 11 or later
  • Devices connected on the same network:
    • Mobile device with Android 8.0 Oreo or higher operating system (OS)
    • Mobile device with SmartThings app installed
    • Matter-enabled SmartThings Station onboarded with Samsung account used for SmartThings app

Initial Setup

  1. Turn on Developer Mode and enable USB debugging option on your mobile device
  2. Install a few OS-specific dependencies by entering the below command in your terminal window:
$ sudo apt-get install git gcc g++ pkg-config libssl-dev libdbus-1-dev \
libglib2.0-dev libavahi-client-dev ninja-build python3-venv python3-dev \
python3-pip unzip libgirepository1.0-dev libcairo2-dev libreadline-dev
  1. To build the Matter virtual device app, install SDK Platform 26 and NDK version 22.1.7171670 using SDK Manager in Android Studio.
  2. After installing NDK, register the NDK path to the Env path.
export ANDROID_NDK_HOME=[NDK PATH]
export PATH=$PATH:${ANDROID_NDK_HOME}
  1. Install Kotlin compiler (kotlinc) version 1.5.10.
  2. After installing kotlinc, register the kotlinc path to the Env path.
export KOTLINC_HOME=[KOTLINC PATH]/bin
export PATH=$PATH:${KOTLINC_HOME}

Sample Code

Here is a sample code for you to start coding in this CodeLab. Download it and start your learning experience!

Matter Virtual Device Sample Code
(11.42 MB)

Start your project

After downloading the sample code containing the project files, open your Android Studio and click Open to open an existing project.

Locate the downloaded Android project from the directory and click OK.

Select and create the device type

When you build and run the sample Matter virtual device app, you can see the already added on/off switch.

Aside from the switch, other types of devices you can create are already prepared in the sample app.

Go to feature > main > java > com.matter.virtual.device.app.feature.main.

In the MainFragment.kt file, select the device type you want to create:

MainUiState.Start -> {
  // ===================================================================================
  // CODELAB
  // TODO : Uncomment the following lines to add your own device type
  // -----------------------------------------------------------------------------------
  val itemList =
    listOf(
      Device.OnOffSwitch,        // Sample
  //    Device.OccupancySensor,    // Level 1 (8+ mins)
  //    Device.ContactSensor,      // Level 2 (8+ mins)
  //    Device.VideoPlayer,        // Level 2 (10+ mins)
  //    Device.DoorLock,           // Level 3 (15+ mins)
  //    Device.ExtendedColorLight, // Level 3 (15+ mins)
  //    Device.WindowCovering,     // Level 4 (17+ mins)
  //    Device.Thermostat,         // Level 5 (20+ mins)
  )
  // ===================================================================================

Depending on the device type you selected at this step, the part you need to modify at the next step will vary. The level of modification complexity is assigned per each device.

Get cluster value

Clusters are the functional building block elements of the data model.

A cluster can be an interface, a service, or an object class, and it is the lowest independent functional element in the data model.

A Matter device supports a set of appropriate clusters, which can interact with your preferred controller (such as SmartThings). This allows for easy information retrieval, behavior setting, event notifications, and more.

Through the ViewModel, get the value of the cluster used in the device you created:

File path: feature > sensor > java > com.matter.virtual.device.app.feature.sensor
File name: OccupancySensorViewModel.kt

// ===================================================================================
// CODELAB Level 1
// The current status of the occupancy. The boolean value is used by the [OccupancyFragment]
// to react to update ui.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _occupancy: StateFlow<Boolean> = getOccupancyFlowUseCase()
//val occupancy: LiveData<Boolean>
//  get() = _occupancy.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// The current status of the battery. The int value is used by the [OccupancyFragment]
// to react to update fragment's UI.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _batteryStatus: MutableStateFlow<Int> =
//  getBatPercentRemainingUseCase() as MutableStateFlow<Int>
//val batteryStatus: LiveData<Int>
//  get() = _batteryStatus.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// Triggered by the "Occupancy" button in the [OccupancyFragment]
// [SetOccupancyUseCase] will update the boolean value of the new occupancy status.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModelScope.launch {
//  Timber.d("current value = ${_occupancy.value}")
//  if (_occupancy.value) {
//    Timber.d("set value = false")
//    setOccupancyUseCase(false)
//  } else {
//    Timber.d("set value = true")
//    setOccupancyUseCase(true)
//  }
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// Triggered by the "Battery" seekbar in the [OccupancyFragment]
// [batteryStatus] store the current status of the battery to indicate the progress.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//_batteryStatus.value = progress
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// Triggered by the "Battery" seekbar in the [OccupancyFragment]
// [updateBatterySeekbarProgress] update the current status of the battery to indicate the
// progress.
// [SetBatPercentRemainingUseCase] will update the int value of the new battery status.
// TODO 5 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModelScope.launch {
//  updateBatterySeekbarProgress(progress)
//  setBatPercentRemainingUseCase(progress)
//}
// ===================================================================================

File path: feature > sensor > java > com.matter.virtual.device.app.feature.sensor
File name: ContactSensorViewModel.kt

// ===================================================================================
// CODELAB Level 2
// The current status of the contact. The boolean value is used by the [ContactSensorFragment]
// to react to update ui.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _stateValue: StateFlow<Boolean> = getStateValueFlowUseCase()
//val stateValue: LiveData<Boolean>
//  get() = _stateValue.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// The current status of the battery. The int value is used by the [ContactSensorFragment]
// to react to update fragment's UI.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _batteryStatus: MutableStateFlow<Int> =
//  getBatPercentRemainingUseCase() as MutableStateFlow<Int>
//val batteryStatus: LiveData<Int>
//  get() = _batteryStatus.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Triggered by the "Contact" button in the [ContactSensorFragment]
// [SetStateValueUseCase] will update the boolean value of the new contact status.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModelScope.launch {
//  Timber.d("current value = ${_stateValue.value}")
//  if (_stateValue.value) {
//    Timber.d("set value = false")
//    setStateValueUseCase(false)
//  } else {
//    Timber.d("set value = true")
//    setStateValueUseCase(true)
//  }
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Triggered by the "Battery" seekbar in the [ContactSensorFragment]
// [batteryStatus] store the current status of the battery to indicate the progress.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//_batteryStatus.value = progress
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Triggered by the "Battery" seekbar in the [ContactSensorFragment]
// [updateBatterySeekbarProgress] update the current status of the battery to indicate the
// progress.
// [SetBatPercentRemainingUseCase] will update the int value of the new battery status.
// TODO 5 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModelScope.launch {
//  updateBatterySeekbarProgress(progress)
//  setBatPercentRemainingUseCase(progress)
//}
// ===================================================================================

File path: feature > media > java > com.matter.virtual.device.app.feature.media
File name: VideoPlayerViewModel.kt

// ===================================================================================
// CODELAB Level 2
// The current status of the on/off. The boolean value is used by the [VideoPlayerFragment]
// to react to update fragment's UI.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _onOff: StateFlow<Boolean> = getOnOffFlowUseCase()
//val onOff: LiveData<Boolean>
//  get() = _onOff.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// The current status of the playback state. The int value is used by the [VideoPlayerFragment]
// to react to update fragment's UI.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _playbackState: StateFlow<Int> = getPlaybackStateFlowUseCase()
//val playbackState: LiveData<Int>
//  get() = _playbackState.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// The current status of the playback speed. The int value is used by the [VideoPlayerFragment]
// to react to update fragment's UI.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _playbackSpeed: StateFlow<Int> = getPlaybackSpeedFlowUseCase()
//val playbackSpeed: LiveData<Int>
//  get() = _playbackSpeed.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// The current status of the key code. The enum value is used by the [VideoPlayerFragment]
// to react to update fragment's UI.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//private val _keyCode: StateFlow<KeyCode> = getKeyCodeStateFlowUseCase()
//val keyCode: LiveData<KeyCode>
//  get() = _keyCode.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Triggered by the "On/Off" button in the [VideoPlayerFragment]
// [SetOnOffUseCase] will update the boolean value of the new on/off status.
// TODO 5 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModelScope.launch {
//  Timber.d("current value = ${_onOff.value}")
//  if (_onOff.value) {
//    Timber.d("set value = false")
//    setOnOffUseCase(false)
//  } else {
//    Timber.d("set value = true")
//    setOnOffUseCase(true)
//  }
//}
// ===================================================================================

File path: feature > closure > java > com.matter.virtual.device.app.feature.closure
File name: DoorLockViewModel.kt

// ===================================================================================
// CODELAB Level 3
// The current status of the lock. The boolean value is used by the [DoorLockFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
private val _lockState: StateFlow<Boolean> = getLockStateFlowUseCase()
val lockState: LiveData<Boolean>
  get() = _lockState.asLiveData()
// ==============================================================================
// ===================================================================================
// CODELAB Level 3
// The current status of the battery. The int value is used by the [DoorLockFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
private val _batteryStatus: MutableStateFlow<Int> =
  getBatPercentRemainingUseCase() as MutableStateFlow<Int>
val batteryStatus: LiveData<Int>
  get() = _batteryStatus.asLiveData()
// ==============================================================================
// ===================================================================================
// CODELAB Level 3
// Triggered by the "Lock" button in the [DoorLockFragment]
// [SetLockStateUseCase] will update the boolean value of the new lock status.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
viewModelScope.launch {
  Timber.d("current lockState value = ${_lockState.value}")
  if (_lockState.value == LOCK_STATE_LOCKED) {
    Timber.d("set value = unlocked")
    setLockStateUseCase(LOCK_STATE_UNLOCKED)
  } else {
    Timber.d("set value = locked")
    setLockStateUseCase(LOCK_STATE_LOCKED)
  }
}
// ==============================================================================
// ===================================================================================
// CODELAB Level 3
// Triggered by the "Send Alarm" button in the [DoorLockFragment]
// [SendLockAlarmEventUseCase] will send alarm event.
// [SetLockStateUseCase] will update the boolean value of the unlock status.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModelScope.launch {
  if (!_lockState.value) {
    // if lockState == locked, send alarm event and change the lockState to unlocked
    sendLockAlarmEventUseCase()
    setLockStateUseCase(LOCK_STATE_UNLOCKED)
  }
}
// ==============================================================================
// ===================================================================================
// CODELAB Level 3
// Triggered by the "Battery" seekbar in the [DoorLockFragment]
// [batteryStatus] store the current status of the battery to indicate the progress.
// -----------------------------------------------------------------------------------

// TODO 5: Copy code below
_batteryStatus.value = progress
// ==============================================================================
// ===================================================================================
// CODELAB Level 3
// Triggered by the "Battery" seekbar in the [DoorLockFragment]
// [updateBatterySeekbarProgress] update the current status of the battery to indicate the
// progress.
// [SetBatPercentRemainingUseCase] will update the int value of the new battery status.
// -----------------------------------------------------------------------------------

// TODO 6: Copy code below
viewModelScope.launch {
  updateBatterySeekbarProgress(progress)
  setBatPercentRemainingUseCase(progress)
}
// ==============================================================================

File path: feature > lighting > java > com.matter.virtual.device.app.feature.lighting
File name: ExtendedColorLightViewModel.kt

// ===================================================================================
// CODELAB Level 3
// The current status of the on/off. The boolean value is used by the [ExtendedColorLightFragment]
// to react to update ui.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
private val _onOff: StateFlow<Boolean> = getOnOffFlowUseCase()
val onOff: LiveData<Boolean>
  get() = _onOff.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// The current status of the color level. The int value is used by the
// [ExtendedColorLightFragment]
// to react to update ui.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
private val _level: StateFlow<Int> = getLevelFlowUseCase()
val level: LiveData<Int>
  get() = _level.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// The current status of the color. The enum value is used by the [ExtendedColorLightFragment]
// to react to update ui.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
private val _currentHue: StateFlow<Int> = getCurrentHueFlowUseCase()
private val _currentSaturation: StateFlow<Int> = getCurrentSaturationFlowUseCase()
val currentColor: LiveData<HsvColor> =
  combine(_currentHue, _currentSaturation) { currentHue, currentSaturation ->
      HsvColor(currentHue, currentSaturation)
    }
    .asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// The current status of the color temperature. The int value is used by the
// [ExtendedColorLightFragment]
// to react to update ui.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
private val _colorTemperature: StateFlow<Int> = getColorTemperatureFlowUseCase()
val colorTemperature: LiveData<Int>
  get() = _colorTemperature.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Triggered by the "On/Off" button in the [ExtendedColorLightFragment]
// [SetOnOffUseCase] will update the boolean value of the new on/off status.
// -----------------------------------------------------------------------------------

// TODO 5: Copy code below
viewModelScope.launch {
  Timber.d("current value = ${_onOff.value}")
  if (_onOff.value) {
    Timber.d("set value = false")
    setOnOffUseCase(false)
  } else {
    Timber.d("set value = true")
    setOnOffUseCase(true)
  }
}
// ===================================================================================

File path: feature > closure > java > com.matter.virtual.device.app.feature.closure
File name: WindowCoveringViewModel.kt

// ===================================================================================
// CODELAB Level 4
// The current status of the position/operation. The enum value is used by the
// [WindowCoveringFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
val windowCoveringStatus: LiveData<WindowCoveringStatus> =
  combine(_currentPosition, _operationalStatus) { currentPosition, operationalStatus ->
      WindowCoveringStatus(currentPosition, operationalStatus)
    }
    .asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// The current status of the battery. The int value is used by the [WindowCoveringFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
private val _batteryStatus: MutableStateFlow<Int> =
  getBatPercentRemainingUseCase() as MutableStateFlow<Int>
val batteryStatus: LiveData<Int>
  get() = _batteryStatus.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "WindowShade" seekbar in the [WindowCoveringFragment]
// [SetTargetPositionUseCase] will update the int value of the new target position status.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
viewModelScope.launch {
  Timber.d("Target position = $percentage")
  setTargetPositionUseCase(percentage)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "Open" button in the [fragment_window_covering.xml]
// [SetTargetPositionUseCase] will update the int value of the open position (100) status.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModelScope.launch {
  Timber.d("Target position = 100")
  setTargetPositionUseCase(100)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "Close" button in the [fragment_window_covering.xml]
// [SetTargetPositionUseCase] will update the int value of the close position (0) status.
// -----------------------------------------------------------------------------------

// TODO 5: Copy code below
viewModelScope.launch { setTargetPositionUseCase(0) }
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "Pause" button in the [fragment_window_covering.xml]
// [SetTargetPositionUseCase] will update the int value of the pause position status.
// -----------------------------------------------------------------------------------

// TODO 6: Copy code below
viewModelScope.launch {
  Timber.d(
    "current position = ${_currentPosition.value}, target position: ${_targetPosition.value}"
  )
  if (_currentPosition.value != _targetPosition.value) {
    setTargetPositionUseCase(_currentPosition.value)
  }
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "Battery" seekbar in the [WindowCoveringFragment]
// [batteryStatus] store the current status of the battery to indicate the progress.
// -----------------------------------------------------------------------------------

// TODO 7: Copy code below
_batteryStatus.value = progress
// ===================================================================================
// ===================================================================================
// CODELAB Level 4
// Triggered by the "Battery" seekbar in the [WindowCoveringFragment]
// [updateBatterySeekbarProgress] update the current status of the battery to indicate the
// progress.
// [SetBatPercentRemainingUseCase] will update the int value of the new battery status.
// -----------------------------------------------------------------------------------

// TODO 8: Copy code below
viewModelScope.launch {
  updateBatterySeekbarProgress(progress)
  setBatPercentRemainingUseCase(progress)
}
// ===================================================================================

File path: feature > hvac > java > com.matter.virtual.device.app.feature.hvac
File name: ThermostatViewModel.kt

// ===================================================================================
// CODELAB Level 5
// The current status of the temperature. The int value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
private val _temperature: MutableStateFlow<Int> =
  getLocalTemperatureUseCase() as MutableStateFlow<Int>
val temperature: LiveData<Int>
  get() = _temperature.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the humidity. The int value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
private val _humidity: MutableStateFlow<Int> =
  getRelativeHumidityUseCase() as MutableStateFlow<Int>
val humidity: LiveData<Int>
  get() = _humidity.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the system mode. The enum value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
private val _systemMode: StateFlow<ThermostatSystemMode> = getSystemModeFlowUseCase()
val systemMode: LiveData<ThermostatSystemMode>
  get() = _systemMode.asLiveData()

// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the fan mode. The enum value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
private val _fanMode: StateFlow<FanControlFanMode> = getFanModeFlowUseCase()
val fanMode: LiveData<FanControlFanMode>
  get() = _fanMode.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the cooling setpoint. The int value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 5: Copy code below
private val _occupiedCoolingSetpoint: StateFlow<Int> = getOccupiedCoolingSetpointFlowUseCase()
val occupiedCoolingSetpoint: LiveData<Int>
  get() = _occupiedCoolingSetpoint.asLiveData()

// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the heating setpoint. The int value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 6: Copy code below
private val _occupiedHeatingSetpoint: StateFlow<Int> = getOccupiedHeatingSetpointFlowUseCase()
val occupiedHeatingSetpoint: LiveData<Int>
  get() = _occupiedHeatingSetpoint.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// The current status of the battery. The int value is used by the [ThermostatFragment]
// to react to update fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 7: Copy code below
private val _batteryStatus: MutableStateFlow<Int> =
  getBatPercentRemainingUseCase() as MutableStateFlow<Int>
val batteryStatus: LiveData<Int>
  get() = _batteryStatus.asLiveData()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Humidity" seekbar in the [ThermostatFragment]
// [humidity] store the current status of the humidity to indicate the progress.
// -----------------------------------------------------------------------------------

// TODO 8: Copy code below
_humidity.value = progress * 100
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Humidity" seekbar in the [ThermostatFragment]
// [updateHumiditySeekbarProgress] update the current status of the humidity to indicate the
// progress.
// [SetRelativeHumidityUseCase] will update the int value of the new humidity status. ([0...100]
// * 100)
// -----------------------------------------------------------------------------------

// TODO 9: Copy code below
viewModelScope.launch {
  updateHumiditySeekbarProgress(progress)
  setRelativeHumidityUseCase(progress * 100)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Temperature" seekbar in the [ThermostatFragment]
// [temperature] store the current status of the temperature to indicate the progress.
// -----------------------------------------------------------------------------------

// TODO 10: Copy code below
_temperature.value = progress * 100
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Temperature" seekbar in the [ThermostatFragment]
// [updateTemperatureSeekbarProgress] update the current status of the temperature to indicate
// the progress.
// [SetLocalTemperatureUseCase] will update the int value of the new temperature status.
// ([value] * 100)
// -----------------------------------------------------------------------------------

// TODO 11: Copy code below
viewModelScope.launch {
  updateTemperatureSeekbarProgress(progress)
  setLocalTemperatureUseCase(progress * 100)
}
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Battery" seekbar in the [ThermostatFragment]
// [batteryStatus] store the current status of the battery to indicate the progress.
// -----------------------------------------------------------------------------------

// TODO 12: Copy code below
_batteryStatus.value = progress
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Battery" seekbar in the [ThermostatFragment]
// [updateBatterySeekbarProgress] update the current status of the battery to indicate the
// progress.
// [SetBatPercentRemainingUseCase] will update the int value of the new battery status.
// -----------------------------------------------------------------------------------

// TODO 13: Copy code below
viewModelScope.launch {
  updateBatterySeekbarProgress(progress)
  setBatPercentRemainingUseCase(progress)
}
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "SystemMode" popup in the [ThermostatFragment]
// [SetSystemModeUseCase] will update the enum value of the new system mode status.
// -----------------------------------------------------------------------------------

// TODO 14: Copy code below
viewModelScope.launch { setSystemModeUseCase(systemMode) }
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "FanMode" popup in the [ThermostatFragment]
// [SetFanModeUseCase] will update the enum value of the new fan mode status.
// -----------------------------------------------------------------------------------

// TODO 15: Copy code below
viewModelScope.launch { setFanModeUseCase(fanMode) }
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Heating Plus" button in the [fragment_thermostat.xml]
// [SetOccupiedHeatingSetpointUseCase] will update the int value of the +1 degree. ([degree] *
// 100)
// -----------------------------------------------------------------------------------

// TODO 16: Copy code below
viewModelScope.launch {
  val nextValue = _occupiedHeatingSetpoint.value + 100
  Timber.d("current value = ${_occupiedHeatingSetpoint.value} set value = $nextValue")
  setOccupiedHeatingSetpointUseCase(nextValue)
}
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Heating Minus" button in the [fragment_thermostat.xml]
// [SetOccupiedHeatingSetpointUseCase] will update the int value of the -1 degree. ([degree] *
// 100)
// -----------------------------------------------------------------------------------

// TODO 17: Copy code below
viewModelScope.launch {
  val nextValue = _occupiedHeatingSetpoint.value - 100
  Timber.d("current value = ${_occupiedHeatingSetpoint.value} set value = $nextValue")
  setOccupiedHeatingSetpointUseCase(nextValue)
}
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Cooling Plus" button in the [fragment_thermostat.xml]
// [SetOccupiedCoolingSetpointUseCase] will update the int value of the +1 degree. ([degree] *
// 100)
// -----------------------------------------------------------------------------------

// TODO 18: Copy code below
viewModelScope.launch {
  val nextValue = _occupiedCoolingSetpoint.value + 100
  Timber.d("current value = ${_occupiedCoolingSetpoint.value} set value = $nextValue")
  setOccupiedCoolingSetpointUseCase(nextValue)
}
// ====================================================================================
// ===================================================================================
// CODELAB Level 5
// Triggered by the "Cooling Minus" button in the [fragment_thermostat.xml]
// [SetOccupiedCoolingSetpointUseCase] will update the int value of the -1 degree. ([degree] *
// 100)
// -----------------------------------------------------------------------------------

// TODO 19: Copy code below
viewModelScope.launch {
  val nextValue = _occupiedCoolingSetpoint.value - 100
  Timber.d("current value = ${_occupiedCoolingSetpoint.value} set value = $nextValue")
  setOccupiedCoolingSetpointUseCase(nextValue)
}
// ====================================================================================

Observe cluster value

Next, use the observe() function to keep track whenever there is a change in the cluster value:

File path: feature > sensor > java > com.matter.virtual.device.app.feature.sensor
File name: OccupancySensorFragment.kt

// ===================================================================================
// CODELAB Level 1
// Trigger the processing for updating new occupancy state of the virtual device.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//binding.occupancyButton.setOnClickListener { viewModel.onClickButton() }
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new battery state of the
// virtual device.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//binding.occupancySensorBatteryLayout.titleText.text = getString(R.string.battery)
//binding.occupancySensorBatteryLayout.seekbarData =
//  SeekbarData(progress = viewModel.batteryStatus)
//binding.occupancySensorBatteryLayout.seekbar.setOnSeekBarChangeListener(
//  object : SeekBar.OnSeekBarChangeListener {
//    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
//      viewModel.updateBatterySeekbarProgress(progress)
//    }
//
//    override fun onStartTrackingTouch(seekBar: SeekBar) {}
//
//    override fun onStopTrackingTouch(seekBar: SeekBar) {
//      viewModel.updateBatteryStatusToCluster(seekBar.progress)
//    }
//  }
//)
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// Observer on the current occupancy status and react on the fragment's UI.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.occupancy.observe(viewLifecycleOwner) {
//  if (it) {
//    binding.occupancyValueText.text = getString(R.string.occupancy_state_occupied)
//    binding.occupancyButton.setImageResource(R.drawable.ic_occupied)
//  } else {
//    binding.occupancyValueText.text = getString(R.string.occupancy_state_unoccupied)
//    binding.occupancyButton.setImageResource(R.drawable.ic_unoccupied)
//  }
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 1
// Observer on the current battery status and react on the fragment's UI.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.batteryStatus.observe(viewLifecycleOwner) {
//  val text: String = getString(R.string.battery_format, it)
//  binding.occupancySensorBatteryLayout.valueText.text =
//    Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
//}
// ===================================================================================

File path: feature > sensor > java > com.matter.virtual.device.app.feature.sensor
File name: ContactSensorFragment.kt

// ===================================================================================
// CODELAB Level 2
// Trigger the processing for updating new contact state of the virtual device.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//binding.contactButton.setOnClickListener { viewModel.onClickButton() }
// ===================================================================================
/** Battery layout */
// ===================================================================================
// CODELAB Level 2
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new battery state of the
// virtual device.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//binding.contactSensorBatteryLayout.titleText.text = getString(R.string.battery)
//binding.contactSensorBatteryLayout.seekbarData = SeekbarData(progress = viewModel.batteryStatus)
//binding.contactSensorBatteryLayout.seekbar.setOnSeekBarChangeListener(
//  object : SeekBar.OnSeekBarChangeListener {
//    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
//      viewModel.updateBatterySeekbarProgress(progress)
//    }
//
//    override fun onStartTrackingTouch(seekBar: SeekBar) {}
//
//    override fun onStopTrackingTouch(seekBar: SeekBar) {
//      viewModel.updateBatteryStatusToCluster(seekBar.progress)
//    }
//  }
//)
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Observer on the current contact status and react on the fragment's UI.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.stateValue.observe(viewLifecycleOwner) {
//  if (it) {
//    binding.contactValueText.text = getString(R.string.contact_state_close)
//    binding.contactButton.setImageResource(R.drawable.ic_unoccupied)
//  } else {
//    binding.contactValueText.text = getString(R.string.contact_state_open)
//    binding.contactButton.setImageResource(R.drawable.ic_occupied)
//  }
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Observer on the current battery status and react on the fragment's UI.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.batteryStatus.observe(viewLifecycleOwner) {
//  val text: String = getString(R.string.battery_format, it)
//  binding.contactSensorBatteryLayout.valueText.text =
//    Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
}
// ===================================================================================

File path: feature > media > java > com.matter.virtual.device.app.feature.media
File name: VideoPlayerFragment.kt

// ===================================================================================
// CODELAB Level 2
// [ButtonData] Observer on the current on/off status and react on the fragment's UI.
// [OnClickListener] Trigger the processing for updating new on/off state of the virtual device.
// TODO 1 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//binding.videoPlayerOnOffLayout.buttonData =
//  ButtonData(
//    onOff = viewModel.onOff,
//    onText = R.string.on_off_switch_power_on,
//    offText = R.string.on_off_switch_power_off
//  )
//binding.videoPlayerOnOffLayout.button.setOnClickListener { viewModel.onClickButton() }
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Observer on the current playback status and react on the fragment's UI.
// TODO 2 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.playbackState.observe(viewLifecycleOwner) { state ->
//  val stateText = convertPlaybackStateToString(state)
//  Timber.d("playbackState:$state($stateText)")
//  binding.videoPlayerStateLayout.valueText.text = stateText
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Observer on the current playback speed and react on the fragment's UI.
// TODO 3 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.playbackSpeed.observe(viewLifecycleOwner) { speed ->
//  binding.videoPlayerSpeedLayout.valueText.text = speed.toString()
//}
// ===================================================================================
// ===================================================================================
// CODELAB Level 2
// Observer on the current key code and react on the fragment's UI.
// TODO 4 : Uncomment the following code blocks
// -----------------------------------------------------------------------------------
//viewModel.keyCode.observe(viewLifecycleOwner) { keyCode ->
//  binding.videoPlayerKeypadLayout.valueText.text = keyCode.value
//}
// ===================================================================================

File path: feature > closure > java > com.matter.virtual.device.app.feature.closure
File name: DoorLockFragment.kt

// ===================================================================================
// CODELAB Level 3
// [ButtonData] Observer on the current lock status and react on the fragment's UI.
// [OnClickListener] Trigger the processing for updating new lock state of the virtual device.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
binding.doorLockOnOffLayout.buttonData =
  ButtonData(
    onOff = viewModel.lockState,
    onText = R.string.door_lock_unlocked,
    offText = R.string.door_lock_locked
  )
binding.doorLockOnOffLayout.button.setOnClickListener { viewModel.onClickButton() }
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Trigger the processing for sending alarm event.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
binding.doorLockSendAlarmLayout.button.setOnClickListener {
  viewModel.onClickSendLockAlarmEventButton()
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new battery state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
binding.doorLockBatteryLayout.titleText.text = getString(R.string.battery)
binding.doorLockBatteryLayout.seekbarData = SeekbarData(progress = viewModel.batteryStatus)
binding.doorLockBatteryLayout.seekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      viewModel.updateBatterySeekbarProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.updateBatteryStatusToCluster(seekBar.progress)
    }
  }
)
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Observer on the current battery status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModel.batteryStatus.observe(viewLifecycleOwner) {
  val text: String = getString(R.string.battery_format, it)
  binding.doorLockBatteryLayout.valueText.text = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
}
// ===================================================================================

File path: feature > lighting > java > com.matter.virtual.device.app.feature.lighting
File name: ExtendedColorLightFragment.kt

// ===================================================================================
// CODELAB Level 3
// [ButtonData] Observer on the current on/off status and react on the fragment's UI.
// [OnClickListener] Trigger the processing for updating new on/off state of the virtual device.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
binding.extendedColorLightOnOffLayout.buttonData =
  ButtonData(
    onOff = viewModel.onOff,
    onText = R.string.on_off_switch_power_on,
    offText = R.string.on_off_switch_power_off
  )
binding.extendedColorLightOnOffLayout.button.setOnClickListener { viewModel.onClickButton() }
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Observer on the current color level status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
viewModel.level.observe(viewLifecycleOwner) {
  // Min: 2(1%), Max: 255(100%)
  val level: Int = (it.toFloat() / 100 * 255).toInt()
  Timber.d("Level: $it")

  // If level value is 0, user can't distinguish the color.
  // So, set it to half value + half of Max.
  binding.extendedColorLightColorLayout.colorBoard.drawable?.alpha = level / 2 + 127
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Observer on the current color status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
viewModel.currentColor.observe(viewLifecycleOwner) { hsvColor ->
  val rgbColor: Int =
    ColorControlUtil.hue2rgb(
      hsvColor.currentHue.toFloat(),
      hsvColor.currentSaturation.toFloat()
    )

  Timber.d("currentHue:${hsvColor.currentHue},currentSaturation:${hsvColor.currentSaturation}")
  Timber.d("Color: #${Integer.toHexString(rgbColor)}")

  var level: Int? = binding.extendedColorLightColorLayout.colorBoard.drawable?.alpha
  if (level == null) level = 255
  Timber.d("level: $level")

  binding.extendedColorLightColorLayout.colorBoard.setImageDrawable(
    BitmapDrawable(resources, ColorControlUtil.colorBoard(rgbColor))
  )
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 3
// Observer on the current color temperature status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModel.colorTemperature.observe(viewLifecycleOwner) {
  // Min: 2580k(2577k), Max: 7050k(7042k)
  val colorTemperature: Int = 1000000 / it
  val rgbColor: Int = ColorControlUtil.kelvin2rgb(colorTemperature)

  Timber.d("Color Temperature: $colorTemperature $it")
  Timber.d("Color: #${Integer.toHexString(rgbColor)}")

  binding.extendedColorLightColorLayout.colorBoard.setImageDrawable(
    BitmapDrawable(resources, ColorControlUtil.colorBoard(rgbColor))
  )
}
// ===================================================================================

File path: feature > closure > java > com.matter.virtual.device.app.feature.closure
File name: WindowCoveringFragment.kt

// ===================================================================================
// CODELAB Level 4
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new WindowShade state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
binding.windowCoveringWindowShadeSeekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      val targetPercentage = seekBar.progress
      val text: String =
        getString(R.string.window_covering_window_shade_format, targetPercentage)
      val percentageTextView = binding.windowCoveringWindowShadeValueText
      percentageTextView.text = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.stopMotion(seekBar.progress)
    }
  }
)
// =======================================================================================================
// ===================================================================================
// CODELAB Level 4
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new battery state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
binding.windowCoveringBatteryLayout.titleText.text = getString(R.string.battery)
binding.windowCoveringBatteryLayout.seekbarData =
  SeekbarData(progress = viewModel.batteryStatus)
binding.windowCoveringBatteryLayout.seekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      viewModel.updateBatterySeekbarProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.updateBatteryStatusToCluster(seekBar.progress)
    }
  }
)

// =======================================================================================================
// ===================================================================================
// CODELAB Level 4
// Observer on the current position/operation status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
viewModel.windowCoveringStatus.observe(viewLifecycleOwner) { status ->
  Timber.d(
    "currentPosition:${status.currentPosition},operationalStatus:${status.operationalStatus}"
  )
  binding.windowCoveringWindowShadeSeekbar.progress = status.currentPosition

  val text: String =
    getString(R.string.window_covering_window_shade_format, status.currentPosition)
  binding.windowCoveringWindowShadeValueText.text =
    Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)

  when (status.operationalStatus) {
    0 -> {
      when (status.currentPosition) {
        0 -> {
          binding.windowCoveringOperationalStatusText.setText(R.string.window_covering_closed)
        }
        100 -> {
          binding.windowCoveringOperationalStatusText.setText(R.string.window_covering_open)
        }
        else -> {
          binding.windowCoveringOperationalStatusText.setText(
            R.string.window_covering_partially_open
          )
        }
      }
    }
    1 -> {
      binding.windowCoveringOperationalStatusText.setText(R.string.window_covering_opening)
    }
    2 -> {
      binding.windowCoveringOperationalStatusText.setText(R.string.window_covering_closing)
    }
    else -> {}
  }
}
// =======================================================================================================
// ===================================================================================
// CODELAB Level 4
// Observer on the current battery status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModel.batteryStatus.observe(viewLifecycleOwner) {
  val text: String = getString(R.string.battery_format, it)
  binding.windowCoveringBatteryLayout.valueText.text =
    Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
}
// =======================================================================================================

File path: feature > hvac > java > com.matter.virtual.device.app.feature.hvac
File name: ThemostatFragment.kt

// ===================================================================================
// CODELAB Level 5
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new temperature state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 1: Copy code below
binding.thermostatTemperatureSeekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      viewModel.updateTemperatureSeekbarProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.updateTemperatureToCluster(seekBar.progress)
    }
  }
)
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new humidity state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 2: Copy code below
binding.humiditySensorHumiditySeekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      viewModel.updateHumiditySeekbarProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.updateHumidityToCluster(seekBar.progress)
    }
  }
)
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// [onProgressChanged] will update the fragment's UI via viewmodel livedata
// [onStopTrackingTouch] will trigger the processing for updating new battery state of the
// virtual device.
// -----------------------------------------------------------------------------------

// TODO 3: Copy code below
binding.thermostatBatteryLayout.titleText.text = getString(R.string.battery)
binding.thermostatBatteryLayout.seekbarData = SeekbarData(progress = viewModel.batteryStatus)
binding.thermostatBatteryLayout.seekbar.setOnSeekBarChangeListener(
  object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
      viewModel.updateBatterySeekbarProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}

    override fun onStopTrackingTouch(seekBar: SeekBar) {
      viewModel.updateBatteryStatusToCluster(seekBar.progress)
    }
  }
)
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current temperature status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 4: Copy code below
viewModel.temperature.observe(viewLifecycleOwner) {
  val celsiusTemp: Float = it.toFloat() / 100

  val celsiusText: String = getString(R.string.temperature_celsius_format, celsiusTemp)
  binding.thermostatTemperatureCelsiusValueText.text =
    Html.fromHtml(celsiusText, Html.FROM_HTML_MODE_LEGACY)

  val fahrenheitTemp: Float = it.toFloat() / 100 * 9 / 5 + 32
  val fahrenheitText: String = getString(R.string.temperature_fahrenheit_format, fahrenheitTemp)
  binding.thermostatTemperatureFahrenheitValueText.text =
    Html.fromHtml(fahrenheitText, Html.FROM_HTML_MODE_LEGACY)

  binding.thermostatTemperatureSeekbar.progress = celsiusTemp.toInt()
}
// ==========================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current fan mode status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 5: Copy code below
viewModel.fanMode.observe(viewLifecycleOwner) {
  Timber.d("fanMode:$it")
  this.fanMode = it
  binding.fanControlFanModeLayout.valueText.text = convertFanModeToString(it)
}
// ==========================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current heating setpoint and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 6: Copy code below
viewModel.occupiedHeatingSetpoint.observe(viewLifecycleOwner) {
  val celsiusTemp: Float = it.toFloat() / 100

  val celsiusText: String = getString(R.string.temperature_celsius_format, celsiusTemp)
  binding.thermostatSetTemperatureHeatingCelsiusValueText.text =
    Html.fromHtml(celsiusText, Html.FROM_HTML_MODE_LEGACY)

  val fahrenheitTemp: Float = it.toFloat() / 100 * 9 / 5 + 32
  val fahrenheitText: String = getString(R.string.temperature_fahrenheit_format, fahrenheitTemp)
  binding.thermostatSetTemperatureHeatingFahrenheitValueText.text =
    Html.fromHtml(fahrenheitText, Html.FROM_HTML_MODE_LEGACY)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current cooling setpoint and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 7: Copy code below
viewModel.occupiedCoolingSetpoint.observe(viewLifecycleOwner) {
  val celsiusTemp: Float = it.toFloat() / 100

  val celsiusText: String = getString(R.string.temperature_celsius_format, celsiusTemp)
  binding.thermostatSetTemperatureCoolingCelsiusValueText.text =
    Html.fromHtml(celsiusText, Html.FROM_HTML_MODE_LEGACY)

  val fahrenheitTemp: Float = it.toFloat() / 100 * 9 / 5 + 32
  val fahrenheitText: String = getString(R.string.temperature_fahrenheit_format, fahrenheitTemp)
  binding.thermostatSetTemperatureCoolingFahrenheitValueText.text =
    Html.fromHtml(fahrenheitText, Html.FROM_HTML_MODE_LEGACY)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current system mode status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 8: Copy code below
viewModel.systemMode.observe(viewLifecycleOwner) {
  Timber.d("systemMode:$it")
  this.systemMode = it
  binding.thermostatSystemModeLayout.valueText.text = convertSystemModeToString(it)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current humidity status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 9: Copy code below
viewModel.humidity.observe(viewLifecycleOwner) {
  val humidity: Int = it / 100
  val humidityText: String = getString(R.string.humidity_format, humidity)
  binding.humiditySensorHumidityPercentageValueText.text =
    Html.fromHtml(humidityText, Html.FROM_HTML_MODE_LEGACY)
  binding.humiditySensorHumiditySeekbar.progress = humidity
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Observer on the current battery status and react on the fragment's UI.
// -----------------------------------------------------------------------------------

// TODO 10: Copy code below
viewModel.batteryStatus.observe(viewLifecycleOwner) {
  val text: String = getString(R.string.battery_format, it)
  binding.thermostatBatteryLayout.valueText.text =
    Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
}
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Trigger the processing for setting system mode.
// -----------------------------------------------------------------------------------

// TODO 11: Copy code below
AlertDialog.Builder(requireContext())
  .setTitle(R.string.thermostat_mode)
  .setSingleChoiceItems(modeList, convertSystemModeToIndex(this.systemMode)) { dialog, which ->
    Timber.d("Thermostat mode set $which(${modeList[which]})")
    viewModel.setSystemMode(convertIndexToSystemMode(which))
    dialog.dismiss()
  }
  .setNegativeButton(R.string.cancel, null)
  .show()
// ===================================================================================
// ===================================================================================
// CODELAB Level 5
// Trigger the processing for setting fan mode.
// -----------------------------------------------------------------------------------

// TODO 12: Copy code below
AlertDialog.Builder(requireContext())
  .setTitle(R.string.fan_control_fan_mode)
  .setSingleChoiceItems(modeList, convertFanModeToIndex(this.fanMode)) { dialog, which ->
    Timber.d("Fan mode set $which(${modeList[which]})")
    viewModel.setFanMode(convertIndexToFanMode(which))
    dialog.dismiss()
  }
  .setNegativeButton(R.string.cancel, null)
  .show()
// ===================================================================================

Build and run the virtual device app

To build and run your app, follow these steps:

  1. Using a USB cable, connect your mobile device. The minimum OS requirement is Android 8.0 (Oreo).
  2. Select the sample virtual device app from the run configurations menu in the Android Studio.
  3. Then, select the connected device in the target device menu.
  4. Click Run.


You can see the device you created in the Matter Virtual Device app.

Onboard and control the virtual device via the SmartThings app

Onboard

To onboard the virtual device:

  1. Select and set up the virtual device type you created. Click Save.
  2. Click Start to show the QR Code for onboarding.

  1. Then, go to the SmartThings app and click the + button.
  2. Then, onboard the virtual device by scanning its QR Code.

Control by SmartThings app

In the SmartThings app, control the virtual device by using its various functions such as on and off for switch.

Contribute to Matter open source (optional)

To contribute to Matter open source, you need to have the latest code. Therefore, apart from the project files provided by this Code Lab activity, you should fork and modify the latest code from Matter open source project.

Matter follows the "Fork-and-Pull" model for accepting contributions. To do this:

  1. Sign in or sign up to GitHub.
  2. Fork the Matter repository by clicking Fork on the web UI.

  1. For each new feature, clone your fork to the local PC and create a working branch:
$ git clone https://github.com/<username>/connectedhomeip.git
$ git checkout –b <branch-name>
  1. Before running the build command, source the environment setup script (activate.sh) at the top level. This script takes care of downloading GN, ninja, and setting up a Python environment with libraries used to build and test.
$ source scripts/activate.sh
  1. Build the virtual device app using the build_example.py.
$ ./scripts/build/build_examples.py --target android-arm64-virtual-device-app build 
  1. Add each modified file to include in the commit. Then, create a commit.
$ git add <filename1> <filename1>
$ git commit –s
  1. Push to your GitHub fork.
$ git push origin <branch-name>
  1. Then, submit your pull request by clicking Contribute > Open pull request on the web UI.

  1. Write a description of the problem, change overview, and test.
  2. Then, sign the Contributor License Agreement (CLA) so a reviewer can automatically be assigned. Click Open pull request.

You're done!

Congratulations! You have successfully achieved the goal of this Code Lab topic. Now, you can create a Matter-compatible virtual device and contribute your code to Matter open source by yourself! If you're having trouble, you may download this file:

Matter Virtual Device Complete Code
(11.49 MB)

Learn more by going to SmartThings Matter libraries.