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},
);
}
}
'''