Introduction

Disclaimer: the documentation is not complete at the moment and even though I strive to update it, I don’t have enough time right now to keep it perfect.

What Is It?

I’m writing something similar to Home Assistant from ground up for my tiny set of “smart” devices.

And no, I didn’t think about the project name long enough.

Why?

  • I want to learn Rust
  • I want it to be as less configurable as possible
  • I want it to run fast on my Raspberry Pi Zero W

Stack

Installation

There are a few different ways to install My IoT. Either way, you get a single executable my-iot:

  • Grab a prebuilt binary from the GitHub releases: https://github.com/eigenein/my-iot-rs/releases
  • Install from crates.io: cargo install my-iot
  • Cross-compile: cargo install cross && cross build --target …

File Capabilities

You may need to manually set the file capabilities on the produced binary:

setcap cap_net_raw+ep /home/pi/.cargo/bin/my-iot

This is needed to use some low-level protocols (for instance, ICMP) as a non-root user.

Settings

My IoT is configured using TOML files specified as command-line arguments:

my-iot my-iot.toml

Example

# my-iot.toml

http_port = 8080

# `heartbeat` is a user-defined service ID.
[services.heartbeat]
type = "Clock"
interval_millis = 2000

[services.weather]
type = "Buienradar"
station_id = 6240

Securing Secrets

It’s a common pattern to split configuration into non-secret and secret parts, where non-secret part is stored under a version control.

my-iot allows specifying multiple settings files, it means that you can put your secrets in a separate file excluded by .gitignore. Services provide separate configuration section to allow moving it out of public part.

For example:

# my-iot.toml:
[services.telegram]
type = "Telegram"

[services.sun_amsterdam]
type = "Solar"
room_title = "Amsterdam"

# secrets.toml:
[services.telegram.secrets]
token = "..."

[services.sun_amsterdam.secrets]
latitude = 52.3667
longitude = 4.8945

Then you run My IoT as my-iot my-iot.toml secrets.toml.

Run at System Startup

For now please refer to Raspberry Pi systemd page.

Example

cat /lib/systemd/system/my-iot.service
[Unit]
Description = my-iot
BindsTo = network-online.target
After = network.target network-online.target

[Service]
ExecStart = /home/pi/bin/my-iot --silent --suppress-log-timestamps my-iot/my-iot.toml my-iot/secrets.toml
WorkingDirectory = /home/pi
StandardOutput = journal
StandardError = journal
Restart = always
User = pi

[Install]
WantedBy = multi-user.target
sudo systemctl enable my-iot
sudo systemctl status my-iot
sudo systemctl start my-iot
sudo systemctl stop my-iot
sudo systemctl restart my-iot

Logs

journalctl -u my-iot -f

See also: How To Use Journalctl to View and Manipulate Systemd Logs.

Publish on the Internet

Checklist

  • Configure Let’s Encrypt or another certificate provider
  • Set the right certificate and private key paths
  • Generate .htpasswd or configure another way of authentication

Example /etc/nginx/nginx.conf

events { }

http {
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        gzip on;

        auth_basic "My IoT";
        auth_basic_user_file /etc/.htpasswd;

        location / {
            proxy_pass http://127.0.0.1:8081;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Services

Service is a kind of interface between My IoT and the real world. You can set up as many services as you want, even multiple services of a same type. A service is typically capable of:

  • Producing messages about something is happening
  • Listening to other services messages and reacting on them

Rhai Scripting

Rhai provides a way to automate things. A script listens to messages in the system and reacts by emitting another messages and/or by calling service methods.

Take also a look at the Cookbook for examples.

Callback on_message

Additional Global Functions

spawn_process(program, args)

error(message) and warning(message)

Additional String Functions

starts_with(another)

Telegram in Rhai

Available Methods

send_message(chat_id, text, options)

send_video(chat_id, video, options)

Ring

Note: uses the unofficial API.

Setting up

Here I use HTTPie for the http command.

First, log in with your email and password:

http 'https://oauth.ring.com/oauth/token' scope=client client_id=ring_official_android grant_type=password username="…" password='…'

If you have the 2-factor authentication enabled (you do, right?), you’ll get HTTP 412 back and an SMS with a code:

HTTP/1.1 412 Precondition Failed

{
    "next_time_in_secs": 60,
    "phone": "+3xxxxxxxx44"
}

Repeat the log-in step with the additional headers added:

http 'https://oauth.ring.com/oauth/token' scope=client client_id=ring_official_android grant_type=password  username="…" password='…' 2fa-support:true 2fa-code:123456

You should get a refresh_token back, which you then specify as the service initial_refresh_token setting value:

{
    "access_token": "…",
    "expires_in": 3600,
    "refresh_token": "…",
    "scope": "client",
    "token_type": "Bearer"
}

My IoT doesn’t need an access_token, because it will obtain a new one immediately via a refresh_token.

Solar

Provides sensors with durations to and after sunrise and sunset:

  • {service_id}::before::sunrise
  • {service_id}::after::sunrise
  • {service_id}::before::sunset
  • {service_id}::after::sunset

For polar night and day the following non-logged messages get emitted:

  • {service_id}::polar::day
  • {service_id}::polar::night

Settings

[services.my_solar]
type = "Solar"

# Room title used for the sensors.
room_title = "Home"

# Refresh interval.
interval_millis = 60000

# Sensor reading expiration time.
ttl_millis = 120000

[services.my_solar.secrets]
latitude = 12.345678
longitude = 12.345678

tado°

tado° Smart Thermostat service, periodically polls the API.

Open Window Detection Skill

This service can emulate the Open Window Detection Skill with the enable_open_window_detection_skill = true setting.

As soon as open window is detected, the service automatically activates the open window mode and emits {service_id}::{home_id}::{zone_id}::open_window_activated message.

Settings

[services.my_tado]
type = "Tado"

# Enables the Open Window Detection Skill emulation.
enable_open_window_detection_skill = true

[services.my_tado.secrets]

# Account email.
email = "user@example.com"

# Account password.
password = "example"

Telegram

Implements a Telegram Bot.

Settings

You’ll need an authorization token:

[services.service_id]
type = "Telegram"

[services.service_id.secrets]
token = "123456789:FoObAr"

Cookbook

Notify tado° open window

[services.notify_open_window]
type = "Rhai"
script = '''
    fn on_message(message) {
        if message.sensor_id == "tado::469375::1::open_window_activated" {
            telegram.send_message(
                -1001349838037,
                "💨 Открыто окно в *" + message.location + "* @eigenein",
                #{parse_mode: "MarkdownV2"},
            );
        }
    }
'''

«Rise and shine» IKEA Trådfri lights starting one hour before sunset

At the moment of writing the recipe there is no native Tradfri service. I’m following the coap-client tutorial to control bulbs.

[services.sun_vijfhuizen]
type = "Solar"
latitude = 52.000000
longitude = 4.000000
room_title = "Vijfhuizen"

[services.rise_and_shine]
type = "Rhai"
script = '''
    fn on_message(message) {
        if message.sensor_id == "sun_vijfhuizen::before::sunset" && message.value.inner < 3600.0 {
            let brightness = 255 * (3600 - message.value.inner.to_int()) / 3600;
            print("Brightness: " + brightness);
            spawn_process("coap-client", [
                "-m",
                "put",
                "-u",
                "eigenein",
                "-k",
                "2GOjFumz6iVnecdt",
                "-e",
                "{\"5851\": " + brightness + "}",
                "coaps://GW-A0C9A0679CBB.home:5684/15004/131080",
            ]);
        }
    }
'''

Send Ring doorbell videos to Telegram

[services.notify_ring]
type = "Rhai"
script = '''
    const chat_id = …;

    fn on_message(message) {
        if message.sensor_id.starts_with("ring::doorbot::32333947::recording::") {
            telegram.send_video(
                chat_id,
                message.value.inner,
                #{caption: "🎥 " + message.location},
            );
        }
    }
'''