Contributing
Contributions of any sort are more than welcome, so we hope this short introduction will help you to get started! Shortly put: we use black to format our code, isort to sort our imports, pytest to test our code, flake8 to do its checks, and doc8 for documentation checks.
See Development environment for setting up a development environment, and Improving device support for some helpful tips for adding support for new devices.
Development environment
This section will shortly go through how to get you started with a working development environment. We use poetry for managing the dependencies and packaging, so simply execute:
poetry install
If you were not already inside a virtual environment during the install,
poetry will create one for you.
You can execute commands inside this environment by using poetry run <command>
,
or alternatively,
enter the virtual environment shell by executing poetry shell
to avoid repeating poetry run
.
To verify the installation, you can launch tox to run all the checks:
tox
In order to make feedback loops faster, we automate our code checks by using precommit hooks. Therefore the first step after setting up the development environment is to install them:
pre-commit install
You can always execute the checks also without doing a commit.
Code checks
Instead of running all available checks during development, it is also possible to execute only the code checks by calling. This will execute the same checks that would be done automatically by precommit when you make a commit:
tox -e lint
Tests
We prefer to have tests for our code, so we use pytest you can also use by executing:
pytest miio
When adding support for a new device or extending an already existing one, please do not forget to create tests for your code.
Generating documentation
You can compile the documentation and open it locally in your browser:
sphinx-build docs/ generated_docs
$BROWSER generated_docs/index.html
Replace $BROWSER with your preferred browser, if the environment variable is not set.
Improving device support
Whether adding support for a new device or improving an existing one, the journey begins by finding out the commands used to control the device. This usually involves capturing packet traces between the device and the official app, and analyzing those packet traces afterwards.
Traffic Capturing
The process is as follows:
Install Android emulator (BlueStacks emulator has been reported to work on Windows).
Install the official Mi Home app in the emulator and set it up to use your device.
Install WireShark (or use
tcpdump
on Linux) to capture the device traffic.Use the app to control the device and save the resulting PCAP file for later analyses.
Obtain the device token in order to decrypt the traffic.
Use
miiocli devtools parse-pcap
script to parse the captured PCAP files.
Note
You can pass as many tokens you want to parse-pcap
, they will be tested sequentially until decryption succeeds,
or the input list is exhausted.
$ miiocli devtools parse-pcap captured_traffic.pcap <token> <another_token>
host -> strip {'id': 6489, 'method': 'get_prop', 'params': ['power', 'temperature', 'current', 'mode', 'power_consume_rate', 'wifi_led', 'power_price']}
strip -> host {'result': ['on', 48.91, 0.07, None, 7.69, 'off', 999], 'id': 6489}
host -> vacuum {'id': 8606, 'method': 'get_status', 'params': []}
vacuum -> host {'result': [{'msg_ver': 8, 'msg_seq': 10146, 'state': 8, 'battery': 100, 'clean_time': 966, 'clean_area': 19342500, 'error_code': 0, 'map_present': 1, 'in_cleaning': 0, 'fan_power': 60, 'dnd_enabled': 1}], 'id': 8606}
...
== stats ==
miio_packets: 24
results: 12
== dst_addr ==
...
== src_addr ==
...
== commands ==
get_prop: 3
get_status: 3
set_custom_mode: 2
set_wifi_led: 2
set_power: 2
Testing Properties
Another option for MiIO devices is to try to test which property accesses return a response. Some ideas about the naming of properties can be located from the existing integrations.
The miiocli devtools test-properties
command can be used to perform this testing:
$ miiocli devtools test-properties power temperature current mode power_consume_rate voltage power_factor elec_leakage
Testing properties ('power', 'temperature', 'current', 'mode', 'power_consume_rate', 'voltage', 'power_factor', 'elec_leakage') for zimi.powerstrip.v2
Testing power 'on' <class 'str'>
Testing temperature 49.13 <class 'float'>
Testing current 0.07 <class 'float'>
Testing mode None
Testing power_consume_rate 7.8 <class 'float'>
Testing voltage None
Testing power_factor 0.0 <class 'float'>
Testing elec_leakage None
Found 5 valid properties, testing max_properties..
Testing 5 properties at once (power temperature current power_consume_rate power_factor): OK for 5 properties
Please copy the results below to your report
### Results ###
Model: zimi.powerstrip.v2
Total responsives: 5
Total non-empty: 5
All non-empty properties:
{'current': 0.07,
'power': 'on',
'power_consume_rate': 7.8,
'power_factor': 0.0,
'temperature': 49.13}
Max properties: 5
MiOT devices
For MiOT devices it is possible to obtain the available commands from the cloud.
The git repository contains a script, devtools/miottemplate.py
, that allows both
downloading the description files and parsing them into more understandable form.
Development checklist
All device classes are derived from either
Device
(for MiIO) orMiotDevice
(for MiOT) (Minimal example).All commands and their arguments should be decorated with
@command
decorator, which will make them accessible to miiocli (miiocli integration).All implementations must either include a model-keyed
_mappings
list (for MiOT), or define_supported_models
variable in the class (for MiIO). listing the known models (as reported byinfo()
).Status containers is derived from
DeviceStatus
class and all properties should have type annotations for their return values. The information that should be exposed directly to end users should be decorated using appropriate decorators (e.g., @sensor or @setting) to make them discoverable (Status containers).Add tests at least for the status container handling (Adding tests).
Updating documentation is generally not needed as the API documentation will be generated automatically.
Minimal example
Todo
Add or link to an example.
miiocli integration
All user-exposed methods of the device class should be decorated with
miio.click_common.command()
to provide console interface.
The decorated methods will be exposed as click commands for the given module.
For example, the following definition:
@command(
click.argument("string_argument", type=str),
click.argument("int_argument", type=int, required=False)
)
def command(string_argument: str, int_argument: int):
click.echo(f"Got {string_argument} and {int_argument}")
Produces a command miiocli example
command requiring an argument
that is passed to the method as string, and an optional integer argument.
Status containers
The status container (returned by the status()
method of the device class)
is the main way for library users to access properties exposed by the device.
The status container should inherit DeviceStatus
.
Doing so ensures that a developer-friendly __repr__()
based on the defined
properties is there to help with debugging.
Furthermore, it allows defining meta information about properties that are especially interesting for end users.
The miiocli
tool will automatically use the defined information to generate a user-friendly output.
Note
The helper decorators are just syntactic sugar to create the corresponding descriptor classes and binding them to the status class.
Note
The descriptors are merely hints to downstream users about the device capabilities. In practice this means that neither the input nor the output values of functions decorated with the descriptors are enforced automatically by this library.
Embedding Containers
Sometimes your device requires multiple I/O requests to gather information you want to expose to downstream users. One example of such is Roborock vacuum integration, where the status request does not report on information about consumables.
To make it easy for downstream users, you can embed other status container classes into a single
one using miio.devicestatus.DeviceStatus.embed()
.
This will create a copy of the exposed descriptors to the main container and act as a proxy to give
access to the properties of embedded containers.
Sensors
Use @sensor
to create SensorDescriptor
objects for the status container.
This will make all decorated sensors accessible through sensors()
for downstream users.
@property
@sensor(name="Voltage", unit="V", some_kwarg_for_downstream="hi there")
def voltage(self) -> Optional[float]:
"""Return the voltage, if available."""
Note
All keywords arguments not defined in the decorator signature will be available
through the extras
variable.
This information can be used to pass information to the downstream users,
see the source of miio.powerstrip.PowerStripStatus
for example of how to pass
device class information to Home Assistant.
Settings
Use @setting
to create SettingDescriptor()
objects.
This will make all decorated settings accessible through settings()
for downstream users.
The type of the descriptor depends on the input parameters:
Passing min_value or max_value will create a
NumberSettingDescriptor
, which is useful for presenting ranges of values.Passing an
enum.Enum
object using choices will create aEnumSettingDescriptor
, which is useful for presenting a fixed set of options.Otherwise, the setting is considered to be boolean switch.
You can either use setter to define a callable that can be used to adjust the value of the property,
or alternatively define setter_name which will be used to bind the method during the initialization
to the the setter()
callable.
Numerical Settings
The number descriptor allows defining a range of values and information about the steps. range_attribute can be used to define an attribute that is used to read the definitions, which is useful when the values depend on a device model.
class ExampleStatus(DeviceStatus):
@property
@setting(name="Color temperature", range_attribute="color_temperature_range")
def colortemp(): ...
class ExampleDevice(Device):
def color_temperature_range() -> ValidSettingRange:
return ValidSettingRange(0, 100, 5)
Alternatively, min_value, max_value, and step can be used.
The max_value is the only mandatory parameter. If not given, min_value defaults to 0
and step to 1
.
@property
@setting(name="Fan Speed", min_value=0, max_value=100, step=5, setter_name="set_fan_speed")
def fan_speed(self) -> int:
"""Return the current fan speed."""
Enum-based Settings
If the device has a setting with some pre-defined values, you want to use this.
class LedBrightness(Enum):
Dim = 0
Bright = 1
Off = 2
@property
@setting(name="LED Brightness", choices=SomeEnum, setter_name="set_led_brightness")
def led_brightness(self) -> LedBrightness:
"""Return the LED brightness."""
Actions
Use @action
to create ActionDescriptor
objects for the device.
This will make all decorated actions accessible through actions()
for downstream users.
@command()
@action(name="Do Something", some_kwarg_for_downstream="hi there")
def do_something(self):
"""Execute some action on the device."""
Note
All keywords arguments not defined in the decorator signature will be available
through the extras
variable.
This information can be used to pass information to the downstream users.
Adding tests
Todo
Describe how to create tests. This part of documentation needs your help! Please consider submitting a pull request to update this.
Documentation
Todo
Describe how to write documentation. This part of documentation needs your help! Please consider submitting a pull request to update this.