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’re few different ways to install My IoT. Either way, you get a single executable my-iot.
File Capabilities
You may need to manually set 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.
Pre-compiled binaries for Raspberry Pi Zero W
curl -fsSL https://raw.githubusercontent.com/eigenein/my-iot-rs/master/install.sh | bash
Install from crates.io
cargo install my-iot
Cross-compile for Raspberry Pi Zero W
make docker/build/arm-unknown-linux-gnueabihf
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 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 right certificate and private key paths
- Generate
.htpasswdor configure another way of authentication
Example
events { }
http {
upstream backend {
server 127.0.0.1:8081;
keepalive 32;
}
server {
listen 443 ssl default_server;
listen [::]:443 default_server;
charset utf-8;
add_header Strict-Transport-Security max-age=2592000;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
gzip on;
gzip_buffers 16 8k;
gzip_comp_level 6;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml
text/javascript application/javascript application/x-javascript
text/x-json application/json application/x-web-app-manifest+json
text/css text/plain text/x-component
font/opentype application/x-font-ttf application/vnd.ms-fontobject
image/x-icon;
gzip_disable "msie6";
auth_basic "My IoT";
auth_basic_user_file /etc/.htpasswd;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
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
Telegram in Rhai
Example
let message = new_send_message(-1001349838037, "@eigenein Hello *World*");
message.parse_mode = parse_mode_markdown_v2;
telegram.call(message);
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"
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.call(new_send_message(
-1001349838037,
"💨 Открыто окно в " + message.location + " @eigenein",
));
}
}
'''
«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",
]);
}
}
'''