SIMO.io Quick Start Guide
Set up your wire‑first SIMO.io system: place the hub, create an instance, pair Game Changer boards, and build your first automations.
Welcome to the smart home developers’ paradise. If you can write print('hello') in Python, you’ll feel at home here. No YAML spaghetti—just minimal, readable Python that gets real work done.
Python reads clearly, works quickly, and gets out of your way. Its popularity has grown over the last few decades for good reason. We have used it since the early 2000s and still choose it for serious, clean work.
SIMO.io is built with Python and the Django framework, following Pythonic practices for structure and clarity. It is also designed to be extended in Python. Not YAML, not JSON, not JavaScript—just Python.
If you are new to programming, Python is an easy way to learn. And learning while shaping your home is honest fun.
Welcome to the developers’ paradise. If you can write print('hello'), you’re already in. This is a quick walk: small, friendly Python pieces that fall into place without fuss.
This guide gets you moving quickly. We’ll build a small app named simo_hello: start with a switch, add a dimmer and a sensor, include forms, models, and admin, then define a gateway (auto‑created), optional discovery, a tiny page, REST endpoints, and dynamic settings. A few simple ideas, and you have a complete integration.
Everything here happens on a SIMO.io hub. Two easy doors stand open:
ssh -p 25422 root@l1.simo.io. On site, you can also SSH to the hub’s LAN IP. Fresh hubs ship with no keys; remove keys to revoke access.Nice to have nearby:
/etc/SIMO/hub (you’ll add your app here and see manage.py).workon simo-hub (activates the hub’s Python environment).supervisorctl status all (peek), supervisorctl restart all (refresh), or restart a single service (e.g., simo-gunicorn)./var/log/simo; per‑gateway and per‑component logs are also visible in Admin.Our little app, simo_hello, touches all the right pieces you’ll reuse for any integration:
A SIMO.io integration is a plain Django app. Your hub’s local settings live at /etc/SIMO/settings.py and start by importing the platform defaults:
"""
Django settings for SIMO.io project.
"""
import os
import sys
from simo.settings import * # platform defaults (INSTALLED_APPS, etc.)
SECRET_KEY = '...'
DEBUG = False
BASE_DIR = '/etc/SIMO'
HUB_DIR = os.path.join(BASE_DIR, 'hub')
VAR_DIR = os.path.join(BASE_DIR, '_var')
STATIC_ROOT = os.path.join(VAR_DIR, 'static')
MEDIA_ROOT = os.path.join(VAR_DIR, 'media')
LOG_DIR = os.path.join(VAR_DIR, 'logs')
Because simo.settings is imported, all core settings are already in place, including INSTALLED_APPS. The local file holds overrides. A typical extension looks like this:
# /etc/SIMO/settings.py
INSTALLED_APPS += ['simo_hello']
The app package is a tiny Python package:
# /etc/SIMO/hub/simo_hello/__init__.py
# empty is fine — this makes it a Python package
These entries are enough for Django to discover simo_hello. supervisorctl restart all to refresh services; logs live under /var/log/simo.
Everything is a Component. A light, a sensor, a lock — each is a component with a base type (e.g., switch, dimmer, binary-sensor, rgbw-light, lock, blinds, gate). The SIMO.io app knows how to render and interact with these shapes.
Controllers add behavior to a component. They reuse SIMO.io base types and app widgets (that’s how the mobile app stays happy). A controller validates values, offers friendly helpers (like turn_on()), and translates to/from your device language.
Gateways do the talking. A controller’s send() becomes a small message to its gateway. The gateway performs the I/O (HTTP/TCP/serial/etc.). When the device speaks back, the gateway calls _receive_from_device(). The UI updates; history is saved. You don’t need to touch MQTT yourself.
Imagine a remote switch that speaks in strings: "ON" and "OFF". SIMO.io’s core Switch uses True/False for its actions. A small controller can sit between them and translate both ways.
The idea is simple: a controller class represents the switch locally; the _prepare_for_send() hook turns a local value into the device’s format; the _prepare_for_set() hook turns a device value back into the local format. The gateway (next) carries messages to and from the real device.
# /etc/SIMO/hub/simo_hello/controllers.py
from simo.core.controllers import Switch
from .gateways import HelloGatewayHandler
class StringSwitch(Switch):
name = "String Switch"
gateway_class = HelloGatewayHandler
# Local True/False → device strings
def _prepare_for_send(self, value):
return 'ON' if value is True else 'OFF'
# Device strings → local True/False
def _prepare_for_set(self, value):
if value == 'ON':
return True
if value == 'OFF':
return False
# A gentle fallback for odd values
return bool(value)
The gateway routes values between SIMO.io and real devices. perform_value_send() is your transmit path. When a device reports back, the gateway calls the controller’s _receive_from_device(), which updates state and the UI. Here is a small demo gateway that stores values in an internal map and reflects them back every 3 seconds:
# /etc/SIMO/hub/simo_hello/gateways.py
from simo.core.gateways import BaseObjectCommandsGatewayHandler
from simo.core.forms import BaseGatewayForm
from simo.core.loggers import get_gw_logger
from simo.core.models import Component
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
name = "Hello Protocol"
config_form = BaseGatewayForm
auto_create = True
periodic_tasks = (('catch_values', 3),)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_gw_logger(self.gateway_instance.id)
self._values_on_the_go = {}
def perform_value_send(self, component, value):
# Replace with a real device call in practice
self.logger.info(f"send to {component} => {value}")
# Demo: stash the value; a periodic task will reflect it back
self._values_on_the_go[component.id] = value
def catch_values(self):
self.logger.info("Catch values every 3s")
# Example only: reflect values on the go back to the component
for component_id, value in list(self._values_on_the_go.items()):
comp = Component.objects.filter(pk=component_id).first()
if comp:
self.logger.info(f"receiving value {value} for {comp}")
comp.controller._receive_from_device(value)
self._values_on_the_go.pop(component_id, None)
auto_create = True on gateways so the hub creates them automatically after a restart. Use manual creation only when user input is required before a gateway can operate (for example, credentials or non‑default ports)._receive_from_device() tidies up. The UI follows along.A Gateway entry (“Hello Protocol”) in Admin pairs with a Component (“Hello Switch”). Toggling in the SIMO.io app produces a friendly log here; a real device call in its place makes the same rhythm drive hardware.
auto_create = True, the “Hello Protocol” gateway appears automatically after services restart. Open it in Django Admin to see a live log stream while you interact with the Hello Switch. System service logs live under /var/logs/simo on the hub.Every controller declares the form that configures it. SIMO.io uses standard Django forms end‑to‑end: the same form powers Django Admin and the SIMO.io app. Users never touch YAML or JSON; they fill a friendly form. On save, fields outside the core Component model land in component.config as JSON.
clean_* methods, clean(), and validation errors. Model choices are supported; SIMO.io stores the primary keys under config for you.Let’s give our hello switch a required remote ID. We add a config form and attach it to the controller. The controller’s defaults ensure a predictable starting state.
# /etc/SIMO/hub/simo_hello/forms.py
from django import forms
from simo.core.forms import BaseComponentForm
class HelloSwitchConfigForm(BaseComponentForm):
remote_id = forms.CharField(
max_length=64,
label="Remote ID",
help_text="Identifier used by the remote device/protocol"
)
def clean_remote_id(self):
rid = self.cleaned_data["remote_id"].strip()
if not rid:
raise forms.ValidationError("Remote ID is required.")
# Add any format checks here (e.g., allowed chars)
return rid
# /etc/SIMO/hub/simo_hello/controllers.py
from simo.core.controllers import Switch
from .gateways import HelloGatewayHandler
from .forms import HelloSwitchConfigForm
class StringSwitch(Switch):
name = "String Switch"
gateway_class = HelloGatewayHandler
config_form = HelloSwitchConfigForm
default_config = {"remote_id": ""}
# Local True/False → device strings
def _prepare_for_send(self, value):
return 'ON' if value is True else 'OFF'
# Device strings → local True/False
def _prepare_for_set(self, value):
if value == 'ON':
return True
if value == 'OFF':
return False
return bool(value)
Inside the gateway, read component.config to use the value in device I/O:
# /etc/SIMO/hub/simo_hello/gateways.py
from simo.core.gateways import BaseObjectCommandsGatewayHandler
from simo.core.forms import BaseGatewayForm
from simo.core.loggers import get_gw_logger
from simo.core.models import Component
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
name = "Hello Protocol"
config_form = BaseGatewayForm
auto_create = True
periodic_tasks = (('catch_values', 3),)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_gw_logger(self.gateway_instance.id)
self._values_on_the_go = {}
def perform_value_send(self, component, value):
remote_id = component.config.get('remote_id')
self.logger.info(f"send to {component} (rid={remote_id}) => {value}")
self._values_on_the_go[component.id] = value
def catch_values(self):
for component_id, value in list(self._values_on_the_go.items()):
comp = Component.objects.filter(pk=component_id).first()
if comp:
self.logger.info(f"receiving value {value} for {comp}")
comp.controller._receive_from_device(value)
self._values_on_the_go.pop(component_id, None)
config_form = YourForm. SIMO.io uses it in Django Admin and the SIMO.io app.Component model are stored under component.config as JSON. ConfigFieldsMixin takes care of mapping models to their primary keys.default_config on the controller to seed sensible defaults when adding new components.clean() or clean_* to validate and normalize input. Errors surface inline in the app.self.component.config from controllers, or component.config from gateways.Forms cover most configuration: values land in component.config and the controller/gateway read them. Some integrations, however, need durable, queryable data: discovered devices, playlists, per‑device capabilities, statistics. That is where Django models shine. Register them in Admin for visibility and control; reference them from component forms using ModelChoiceField.
Here’s a minimal registry for remote hello devices. We track remote_id, a friendly label, and liveness. A small signal keeps component alive in sync with the model.
# /etc/SIMO/hub/simo_hello/models.py
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from simo.core.models import Component
class HelloDevice(models.Model):
remote_id = models.CharField(max_length=64, unique=True, db_index=True)
label = models.CharField(max_length=120)
online = models.BooleanField(default=True)
last_seen = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.label} ({self.remote_id})"
@receiver(post_save, sender=HelloDevice)
def update_component_online(sender, instance, created, **kwargs):
if created:
return
from .gateways import HelloGatewayHandler
for comp in Component.objects.filter(
gateway__type=HelloGatewayHandler.uid, base_type='switch'
):
if comp.config.get('hello_device') == instance.id:
comp.alive = instance.online
comp.save(update_fields=['alive'])
# /etc/SIMO/hub/simo_hello/admin.py
from django.contrib import admin
from .models import HelloDevice
@admin.register(HelloDevice)
class HelloDeviceAdmin(admin.ModelAdmin):
list_display = 'remote_id', 'label', 'online', 'last_seen'
search_fields = 'remote_id', 'label'
list_filter = 'online',
readonly_fields = 'last_seen',
Swap the simple string field for a ModelChoiceField. The SIMO.io form serializer stores the selected object’s primary key into component.config automatically. The gateway then resolves it at runtime.
# /etc/SIMO/hub/simo_hello/forms.py
from django import forms
from simo.core.forms import BaseComponentForm
from .models import HelloDevice
class HelloSwitchModelConfigForm(BaseComponentForm):
hello_device = forms.ModelChoiceField(
label='Hello device', queryset=HelloDevice.objects.all()
)
# /etc/SIMO/hub/simo_hello/controllers.py
from simo.core.controllers import Switch
from .gateways import HelloGatewayHandler
from .forms import HelloSwitchModelConfigForm
class StringSwitch(Switch):
name = "String Switch"
gateway_class = HelloGatewayHandler
config_form = HelloSwitchModelConfigForm
default_config = {"hello_device": None}
# /etc/SIMO/hub/simo_hello/gateways.py
from simo.core.gateways import BaseObjectCommandsGatewayHandler
from simo.core.loggers import get_gw_logger
from .models import HelloDevice
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
name = "Hello Protocol"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_gw_logger(self.gateway_instance.id)
def perform_value_send(self, component, value):
dev_pk = component.config.get('hello_device')
device = HelloDevice.objects.filter(pk=dev_pk).first()
if not device:
self.logger.warning("no HelloDevice linked; skipping send")
return
self.logger.info(f"send to {device} => {value}")
# Replace with a real device call...
SIMO.io auto‑discovers simple URL modules in each app. If your app provides an auto_urls.py with a standard urlpatterns list, SIMO.io mounts it automatically at /app_label/.... No project‑level edits required.
auto_urls.py in your app and define urlpatterns. SIMO.io includes it under the app’s label (e.g., /simo_hello/hello-status/).Let’s expose a small status endpoint and a simple HTML page. Place them under standard views.py and wire with auto_urls.py. The resulting URLs will be /simo_hello/hello-status/ and /simo_hello/hello/.
# /etc/SIMO/hub/simo_hello/views.py
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
@login_required
def hello_status(request):
return JsonResponse({"status": "ok"})
@login_required
def hello_page(request):
return render(request, 'simo_hello/hello_page.html', {"title": "Hello"})
# /etc/SIMO/hub/simo_hello/auto_urls.py
from django.urls import path
from .views import hello_status, hello_page
urlpatterns = [
path('hello-status/', hello_status, name='hello-status'),
path('hello/', hello_page, name='hello-page'),
]
# /etc/SIMO/hub/simo_hello/templates/simo_hello/hello_page.html
<div class="container py-4">
<h2 class="mb-3">Hello from simo_hello</h2>
<p class="mb-0">This is a tiny, app‑scoped page.</p>
</div>
@login_required unless you intend them public. Admin helpers often render pages for hub masters.auto_urls under the app label (e.g., simo_hello).templates/app_label/.api.py with DRF viewsets; SIMO.io auto‑registers them under /api/<instance_slug>/ (see REST API below)./var/logs/simo.For JSON APIs, define a DRF ViewSet in your app’s api.py. SIMO.io auto‑registers any viewset that sets url (path prefix) and basename. No per‑app URL wiring — endpoints appear under /api/<instance_slug>/….
# /etc/SIMO/hub/simo_hello/serializers.py
from rest_framework import serializers
from .models import HelloDevice
class HelloDeviceSerializer(serializers.ModelSerializer):
class Meta:
model = HelloDevice
fields = ("id", "remote_id", "label", "online", "last_seen")
# /etc/SIMO/hub/simo_hello/api.py
from rest_framework import viewsets
from .models import HelloDevice
from .serializers import HelloDeviceSerializer
class HelloDeviceViewSet(viewsets.ReadOnlyModelViewSet):
url = 'hello/devices'
basename = 'hello-devices'
queryset = HelloDevice.objects.all().order_by('label')
serializer_class = HelloDeviceSerializer
InstanceMixin from simo.core.api and filter on self.instance in get_queryset(). For hub‑global resources, a plain viewset is fine.GET /api/<instance_slug>/hello/devices/, GET /api/<instance_slug>/hello/devices/{id}/.@action for custom endpoints (e.g., POST operations) when needed.Discovery reduces setup to a single, confident gesture. Instead of typing IDs or hunting IP addresses, you put a device into pairing mode and let the gateway find it. It’s faster on a ladder, kinder to non‑developers, and still explicit: you start discovery, you confirm results, you finish. Local, professional, predictable.
_init_discovery(). The SIMO.io app shows a “Discover” flow and the controller’s discovery_msg to guide the user.gateway.start_discovery() with initial form data. The gateway begins scanning.gateway.process_discovery({...}). The controller’s _process_discovery() may create components or return helpful errors.finish_discovery(). If the controller provides _finish_discovery(), it can finalize any remaining pieces.We’ll add a gentle prompt and two class methods. The controller kicks off discovery; when the gateway finds a device with a remote_id, the controller creates a component from the original form inputs plus the discovered ID.
# /etc/SIMO/hub/simo_hello/controllers.py
from simo.core.controllers import Switch
from simo.core.models import Gateway
from simo.core.utils.serialization import serialize_form_data, deserialize_form_data
from .forms import HelloSwitchConfigForm
from .gateways import HelloGatewayHandler
class StringSwitch(Switch):
name = "String Switch"
gateway_class = HelloGatewayHandler
config_form = HelloSwitchConfigForm
default_config = {"remote_id": ""}
discovery_msg = "Put your Hello device into pairing mode."
@classmethod
def _init_discovery(self, form_cleaned_data):
gateway = Gateway.objects.filter(type=self.gateway_class.uid).first()
gateway.start_discovery(
self.uid, serialize_form_data(form_cleaned_data), timeout=30
)
@classmethod
def _process_discovery(cls, started_with, data):
if data.get('discovery-result') == 'fail':
return { 'error': data.get('error', 'Device not found.') }
started_with = deserialize_form_data(started_with)
started_with['remote_id'] = data['result']['config']['remote_id']
form = HelloSwitchConfigForm(controller_uid=cls.uid, data=started_with)
if form.is_valid():
new_component = form.save()
return [new_component]
return { 'error': form.errors.as_text() }
Your gateway feeds discoveries back to SIMO.io by calling process_discovery() on its Gateway model instance. In a real driver this would parse SSDP/mDNS packets, poll a bus, or listen for advertisements. Below, we demo reading known HelloDevice rows to simulate discovery.
# /etc/SIMO/hub/simo_hello/gateways.py
from simo.core.gateways import BaseObjectCommandsGatewayHandler
from simo.core.loggers import get_gw_logger
from .models import HelloDevice
from .controllers import StringSwitch
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
name = "Hello Protocol"
periodic_tasks = (('discover_hello_devices', 5),)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_gw_logger(self.gateway_instance.id)
self._seen = set()
def discover_hello_devices(self):
for dev in HelloDevice.objects.filter(online=True):
if dev.remote_id in self._seen:
continue
payload = {
'type': StringSwitch.uid,
'discovery-result': 'ok',
'result': { 'config': { 'remote_id': dev.remote_id } },
}
self.gateway_instance.process_discovery(payload)
self._seen.add(dev.remote_id)
Some knobs belong in the app, not in code. Dynamic settings let you tweak behavior per hub instance without redeploying: poll intervals, extra logging, feature flags. They live under a namespace and flow through the same permissions and audit trail as everything else.
<app>__<setting> (double underscore). Example: hello__poll_interval.# /etc/SIMO/hub/simo_hello/dynamic_settings.py
from dynamic_preferences.preferences import Section
from dynamic_preferences.types import IntegerPreference, BooleanPreference
from dynamic_preferences.registries import global_preferences_registry
hello = Section('hello')
@global_preferences_registry.register
class HelloPollInterval(IntegerPreference):
section = hello
name = 'poll_interval'
default = 5 # seconds
help_text = 'How often to run hello discovery/polling.'
@global_preferences_registry.register
class HelloDebug(BooleanPreference):
section = hello
name = 'debug'
default = False
help_text = 'Emit extra gateway debug logs.'
# /etc/SIMO/hub/simo_hello/gateways.py
import time
from simo.conf import dynamic_settings
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
name = "Hello Protocol"
periodic_tasks = (('discover_hello_devices', 1),) # tick fast; we self-throttle
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._last_discovery = 0
def discover_hello_devices(self):
# Throttle by dynamic setting
interval = int(dynamic_settings['hello__poll_interval']) or 5
if time.time() - self._last_discovery < interval:
return
self._last_discovery = time.time()
debug = bool(dynamic_settings['hello__debug'])
if debug:
self.logger.debug("hello discovery tick")
# ... perform discovery here ...
Logs save time. Every gateway and component has its own rotating log on disk and a handy viewer in Admin. Keep logs tidy: debug for noisy details, info for normal operations, warning/error for trouble.
get_gw_logger(gateway_id). View live via Admin’s log widget.get_component_logger(component) when a controller needs its own trail./var/logs/simo for deeper troubleshooting.# /etc/SIMO/hub/simo_hello/gateways.py
from simo.core.loggers import get_gw_logger
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_gw_logger(self.gateway_instance.id)
def perform_value_send(self, component, value):
self.logger.info(f"TX to {component} => {value}")
try:
# ... device I/O ...
pass
except Exception as e:
self.logger.error(f"TX error {e}", exc_info=True)
Networks fail. Devices sulk. Build backoffs and clear status into your gateway. Mark components unhealthy when their device stops answering; recover automatically when it wakes up.
# /etc/SIMO/hub/simo_hello/gateways.py
import time
class HelloGatewayHandler(BaseObjectCommandsGatewayHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._retries = {}
def perform_value_send(self, component, value):
try:
# device_call(component, value)
self._retries.pop(component.id, None)
component.alive = True
component.error_msg = None
component.save(update_fields=['alive', 'error_msg'])
except Exception as e:
backoff = self._retries.get(component.id, 0)
self._retries[component.id] = min(backoff + 1, 5)
self.logger.warning(f"Retry {self._retries[component.id]} for {component}: {e}")
component.alive = False
component.error_msg = str(e)
component.save(update_fields=['alive', 'error_msg'])
The hub records value changes, alarms, and actions. This makes troubleshooting and UX features (graphs, timelines) straightforward.
Publish one sharp module and you lift the whole craft. Your code unblocks an installer on a ladder, saves a weekend for a family, and gives another developer a clean starting point. Put your name on it. Keep the README.rst short and kind. Let people find you!
Publish a small, well‑named Python package so others can install it cleanly on their SIMO.io hubs. Use the simo-<integration> naming pattern to make intent obvious (e.g., simo-hello).
INSTALLED_APPS, and how to restart services.simo-hello/
├─ pyproject.toml
├─ README.rst
└─ src/
└─ simo_hello/
├─ __init__.py
├─ controllers.py
├─ gateways.py
├─ forms.py
└─ dynamic_settings.py
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "simo-hello"
version = "0.1.0"
description = "Hello integration for SIMO.io hub"
readme = "README.rst"
requires-python = ">=3.10"
authors = [{ name = "You", email = "you@example.com" }]
license = { text = "MIT" }
keywords = ["simo.io", "smart-home", "integration"]
classifiers = [
"Programming Language :: Python :: 3",
"Framework :: Django",
]
[project.urls]
Homepage = "https://github.com/you/simo-hello"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["simo_hello*"]
SIMO‑Hello
==========
Install
-------
1. SSH to your SIMO.io hub and activate env::
workon simo-hub
2. Install the package::
pip install simo-hello
3. Enable the app in ``/etc/SIMO/settings.py``::
INSTALLED_APPS += ['simo_hello']
4. Restart services::
supervisorctl restart all
Usage
-----
After you restart services, the "Hello Protocol" gateway is created automatically. Open it in Django Admin to watch logs. Then add a "String Switch" component from the SIMO.io app.
python -m pip install build twine then python -m build.python -m twine upload dist/* (use your token when prompted or configure ~/.pypirc).
Publish your integration so the community can discover, try, and improve it.
simo-<brand-or-device> (e.g., simo-hello).controllers.py, gateways.py, forms.py, pyproject.toml, requirements.txt, README.rst, and a LICENSE.auto_create = True on your gateway.INSTALLED_APPS → migrate → restart → add components → how to use.Set up your wire‑first SIMO.io system: place the hub, create an instance, pair Game Changer boards, and build your first automations.