Mobly enables users to control custom hardware devices (e.g., smart lights, switches) by creating custom controller modules. This tutorial explains how to implement a production-ready custom controller.
A Mobly controller module needs to implement specific top-level functions to manage the device lifecycle.
Required functions:
create(configs): Instantiates controller objects from the configuration.destroy(objects): Cleans up resources when the test ends.
Optional functions:
get_info(objects): Returns device information for the test report. If not implemented, no controller information will be included in the result.
The following example demonstrates a custom controller for a Smart Light, featuring type hinting, input validation, and fault-tolerant cleanup.
Save this code as smart_light.py.
"""Mobly controller module for a Smart Light."""
import logging
from typing import Any, Dict, List
# The key used in the config file to identify this controller.
MOBLY_CONTROLLER_CONFIG_NAME = "SmartLight"
class SmartLight:
"""A class representing a smart light device.
Attributes:
name: the name of the device.
ip: the IP address of the device.
is_on: True if the light is currently on, False otherwise.
"""
def __init__(self, name: str, ip: str):
self.name = name
self.ip = ip
self.is_on = False
logging.info("Initialized SmartLight [%s] at %s", self.name, self.ip)
def power_on(self):
"""Turns the light on."""
self.is_on = True
logging.info("SmartLight [%s] turned ON", self.name)
def power_off(self):
"""Turns the light off."""
self.is_on = False
logging.info("SmartLight [%s] turned OFF", self.name)
def close(self):
"""Simulates closing the connection."""
logging.info("SmartLight [%s] connection closed", self.name)
def create(configs: List[Dict[str, Any]]) -> List[SmartLight]:
"""Creates SmartLight instances from a list of configurations.
Args:
configs: A list of dicts, where each dict represents a configuration
for a SmartLight device.
Returns:
A list of SmartLight objects.
Raises:
ValueError: If a required configuration parameter is missing.
"""
devices = []
for config in configs:
if "name" not in config or "ip" not in config:
raise ValueError(
f"Invalid config: {config}. 'name' and 'ip' are required."
)
devices.append(SmartLight(
name=config["name"],
ip=config["ip"]
))
return devices
def destroy(objects: List[SmartLight]) -> None:
"""Cleans up SmartLight instances.
Args:
objects: A list of SmartLight objects to be destroyed.
"""
for light in objects:
try:
if light.is_on:
light.power_off()
light.close()
except Exception:
# Catching broad exceptions ensures that a failure in one device
# does not prevent others from being cleaned up.
logging.exception("Failed to clean up SmartLight [%s]", light.name)
def get_info(objects: List[SmartLight]) -> List[Dict[str, Any]]:
"""Returns information for the test result.
Args:
objects: A list of SmartLight objects.
Returns:
A list of dicts containing device information.
"""
return [{"name": light.name, "ip": light.ip} for light in objects]To use the custom controller, register it in your test script.
from mobly import base_test
from mobly import test_runner
import smart_light
class LightTest(base_test.BaseTestClass):
def setup_class(self):
# Register the custom controller
self.lights = self.register_controller(smart_light)
def test_turn_on(self):
light = self.lights[0]
light.power_on()
# Verify the light is on
if not light.is_on:
raise signals.TestFailure(f"Light {light.name} should be on!")
if __name__ == "__main__":
test_runner.main()Define the device in your configuration file using the SmartLight key.
TestBeds:
- Name: BedroomTestBed
Controllers:
SmartLight:
- name: "BedLight"
ip: "192.168.1.50"