Running with Ruby

Page 3 of 151

Extracting the device token from Xiaomi Air Purifier 2S EU for Domoticz usage

Xiaomi Air Purifier is one of the best on the market in the price/value category. Like many other Xiaomi devices, it can be controlled using a great home automation system called Domoticz.

The only problem that I had is that for the 2S version, there is no way to obtain the device token needed for controlling the device using the miIO library.

Here are the steps needed to obtain this token using a Linux machine and a non-rooted Android phone.

Getting the Mi Home data backup

  1. Download and install the Mi Home application. You need to have the 5.0.19 version of this app. The newer versions don’t persist the token locally in the SQLite database. You can get it from the APKMirror page. Note, that you will have to allow the Unknown sources to do that. Here’s a description on how to do that.
  2. Enable developer mode and USB debugging on your phone (instruction).
  3. Download and extract the Android Platform tools for Linux from here.
  4. Install the most recent Java version if you don’t have it.
  5. Enable developer mode and USB debugging on your phone and connect it to your computer.
  6. Unlock the phone and allow the computer for data transfers (not USB charging only).
  7. Authorize the Linux machine on your Android phone.
  8. Run the following command from your Linux machine:
    ./adb backup -noapk com.xiaomi.smarthome -f backup.ab
  9. Unlock your device and confirm the backup operation. If your phone is encrypted, you will also have to provide the backup password.
  10. If everything went smooth, you should have a backup.ab file in the platform tools for further processing. From this moment, you won’t need the phone to be connected to your laptop.

Getting the device token out of the Mi Home backup

  1. Download and unpack the ADB extractor.
  2. Navigate to the android-backup-tookit/android-backup-extractor/android-backup-extractor-20180521-bin directory.
  3. Copy the abe.jar file to where your backup.ab is.
  4. Run the following command and follow the instructions (if any):
    java -jar abe.jar unpack backup.ab backup.tar
  5. Unpack the extracted backup:
    tar -xvf backup.tar
  6. Go to the apps/com.xiaomi.smarthome/db directory.
  7. Install and run the DB Browser for SQLite:
    sudo apt-get install sqlitebrowser
  8. Open the miio2.db file using the DB Browsers for SQLite.
  9. Click on the Execute SQL tab and run the following query:
    SELECT localIP, token FROM devicerecord

    As a result, you will get a list of the tokens with the IPs of the devices in your networks to which they belong (I’ve blurred the tokens just in case in the picture):

Now you can take the appropriate token and use it within your Domoticz setup.

Simplifying internal validations using Dry-Validation

When building APIs for other developers, it’s often important to draw the line between other programmers input data and the internal world of your library. This process is called data validation and you’re probably familiar with this name.

What you may not know, is the fact that it can be achieved in many ways.

One that I particularly like is by using the dry-validation library. Here’s an example on how you can separate the validation from the actual business logic without actually changing the API of your library.

The inline way

The easiest way to provide validations is to embed the checks in a place where you receive the data.

This approach is great for super simple cases like the one below:

def sum(arg1, arg2)
  raise ArgumentError unless arg1
  raise ArgumentError unless arg2

  arg1.to_i + arg2.to_i

sum(2, nil) #=> ArgumentError (ArgumentError)

However, if you decide to follow this road, you will quickly end up with something really unreadable.

This code sample is taken from the ruby-kafka library and it’s used to validate the method input. I’ve removed the business logic parts as they aren’t relevant to the context of this article:

def build(
  ca_cert_file_path: nil,
  ca_cert: nil,
  client_cert: nil,
  client_cert_key: nil,
  client_cert_chain: nil,
  ca_certs_from_system: nil
  return nil unless ca_cert_file_path ||
                    ca_cert ||
                    client_cert ||
                    client_cert_key ||
                    client_cert_chain ||

  if client_cert && client_cert_key
    # business irrelevant to the checks
    if client_cert_chain
      # business irrelevant to the checks
  elsif client_cert && !client_cert_key
    raise ArgumentError, "initialized with ssl_client_cert` but no ssl_client_cert_key"
  elsif !client_cert && client_cert_key
    raise ArgumentError, "initialized with ssl_client_cert_key, but no ssl_client_cert"
  elsif client_cert_chain && !client_cert
    raise ArgumentError, "initialized with ssl_client_cert_chain, but no ssl_client_cert"
  elsif client_cert_chain && !client_cert_key
    raise ArgumentError, "initialized with ssl_client_cert_chain, but no ssl_client_cert_key"

  # business

Despite looking simple, the if-elsif validation is really complex and it brings many things to the table:

  • it checks several variables,
  • mixes the checks together due to the if-flow,
  • in the end it actually only checks the presence of the variables,
  • despite expecting string values, it will work with anything that is provided,
  • it forces us to spec out the validation cases with the business logic as they are coupled together.

Luckily for us, there’s a better way to do that.

The private schema way

We can achieve the same functionality and much more just by extracting the validations into a separate internal class. Let’s build up only the interface for now.

Note, that I’m leaving the ArgumentError and the external API intact, as I don’t want this change to impact anything that is outside of this class:

require 'dry-validation'

# Empty schema for now, we will get there
SCHEMA = Dry::Validation.Schema {}

def build(
  ca_cert_file_path: nil,
  ca_cert: nil,
  client_cert: nil,
  client_cert_key: nil,
  client_cert_chain: nil,
  ca_certs_from_system: nil
  input = {
    ca_cert_file_path: ca_cert_file_path,
    ca_cert: ca_cert,
    client_cert: client_cert,
    client_cert_key: client_cert_key,
    client_cert_chain: client_cert_chain,
    ca_certs_from_syste: ca_certs_from_system

  # Do nothing if there's nothing to do
  return nil if input.values.all?(&:nil?)

  results =
  raise ArgumentError, results.errors unless results.success?

  # Business logic

We’ve managed to extract the validation logic outside.

Thanks to that, now we have:

  • separation of responsibilities,
  • business applying method that we can test against only valid cases,
  • validation object that we can test in isolation,
  • much cleaner API that can be easier expanded (new arguments, new data types supported, etc) and/or replaced,
  • way to handle more complex validations (types, formats, etc),
  • support for reporting multiple issues with the input at the same time.

We can now perform all the checks and only when everything is good, we will run the business. But what about the validation itself?

Actually all the validations below are copy-pasted from the karafka repository. Here’s the dry-validation documentation.

require 'dry-validation'

SCHEMA = Dry::Validation.Schema do
  ].each do |encryption_attribute|


    client_cert_with_client_cert_key: %i[
  ) do |client_cert, client_cert_key|
    client_cert.filled? > client_cert_key.filled?

    client_cert_key_with_client_cert: %i[
  ) do |client_cert, client_cert_key|
    client_cert_key.filled? > client_cert.filled?

    client_cert_chain_with_client_cert: %i[
  ) do |client_cert, client_cert_chain|
    client_cert_chain.filled? > client_cert.filled?

    client_cert_chain_with_client_cert_key: %i[
  ) do |client_cert_chain, client_cert_key|
    client_cert_chain.filled? > client_cert_key.filled?

The execution effect is also really good:

build(ca_cert: 2) #=> {:ca_cert=>["must be a string"]} (ArgumentError)


Whenever you find yourself adding some inline validations, stop and think twice, there’s probably a better and more extendable way to do it.

Originally published at The Castle blog.

« Older posts Newer posts »

Copyright © 2019 Running with Ruby

Theme by Anders NorenUp ↑