Plugin Development Tutorial¶
Step-by-step guide to creating your first Tinko plugin.
What is a Plugin?¶
A Tinko plugin is a self-contained module that extends the platform's functionality. Each plugin has:
- A
plugin.pyfile with aPluginclass - Its own models, views, templates, and static files
- Lifecycle methods for initialization and cleanup
- Automatic discovery and registration
Tutorial: Creating a Weather Station Plugin¶
Let's create a simple weather station plugin that reads temperature and displays it on the web interface.
Step 1: Create Directory Structure¶
Directory structure:
plugins/acme/weather_station/
├── __init__.py # Required
├── plugin.py # Required
├── models.py # Optional: Database models
├── views.py # Optional: Web views
├── urls.py # Optional: URL routes
├── forms.py # Optional: Forms
├── consumers.py # Optional: WebSocket consumers
├── static/ # Optional: CSS/JS
├── templates/ # Optional: HTML templates
└── migrations/ # Auto-created
Step 2: Create init.py¶
File: plugins/acme/weather_station/__init__.py
"""Weather Station Plugin for Tinko."""
try:
from .plugin import Plugin
except ImportError:
pass # Plugin class not available during early Django startup
default_app_config = "plugins.acme.weather_station.apps.WeatherStationConfig"
Step 3: Create Plugin Registration¶
File: plugins/acme/weather_station/plugin.py
"""Weather Station Plugin registration."""
import logging
from typing import Optional
from core.plugin_system.base import PluginBase
logger = logging.getLogger(__name__)
class Plugin(PluginBase):
"""Weather Station Plugin for monitoring temperature and humidity."""
# Plugin metadata
name = "Weather Station"
description = "Monitor temperature and humidity with web dashboard"
author = "Acme Corp"
version = "1.0.0"
icon = "cloud-sun"
def __init__(self, plugin_path: str, enabled: bool = True):
"""Initialize the plugin."""
super().__init__(plugin_path, enabled)
self._sensor = None
self._last_reading = None
def boot(self) -> None:
"""Initialize the plugin when loaded."""
# Register GPIO pins for temperature sensor
self.register_gpio_pins({
'sensor_data': 4, # DHT22 data pin
'status_led': 17, # LED to show activity
})
# Schedule readings every 60 seconds
self.register_schedule(
interval=60,
callback=self.read_sensors,
name='weather_reader'
)
logger.info(f"{self.name} plugin booted successfully")
def register(self) -> None:
"""Register plugin components."""
from django.urls import path, include
# Import models and views here to avoid circular imports
from .models import WeatherReading
from .views import WeatherDashboardView, WeatherAPIView
# Register models
self.register_model(WeatherReading)
# Register URL patterns
self.register_url_pattern(
'',
include('plugins.acme.weather_station.urls'),
name='weather_station'
)
# Register admin menu
self.register_admin_menu(
'Weather Station',
'/plugins/acme/weather_station/',
icon='thermometer-half'
)
# Register settings
self.register_setting(
'update_interval',
'Update Interval (seconds)',
default=60,
field_type='number',
min=30,
max=3600
)
self.register_setting(
'temperature_unit',
'Temperature Unit',
default='celsius',
field_type='select',
choices=[
('celsius', 'Celsius'),
('fahrenheit', 'Fahrenheit'),
]
)
logger.info(f"{self.name} plugin registered")
def uninstall(self) -> None:
"""Clean up when plugin is disabled."""
# Release GPIO pins
self.cleanup_gpio_pins()
logger.info(f"{self.name} plugin uninstalled")
def read_sensors(self) -> None:
"""Read temperature and humidity from sensor."""
try:
# In real implementation, read from DHT22 sensor
# For this tutorial, we'll simulate readings
import random
temperature = 20 + random.uniform(-5, 5) # Simulated: 15-25°C
humidity = 50 + random.uniform(-20, 20) # Simulated: 30-70%
# Store reading in database
from .models import WeatherReading
WeatherReading.objects.create(
temperature=temperature,
humidity=humidity
)
# Update status LED
self._update_status_led()
logger.debug(f"Weather reading: {temperature:.1f}°C, {humidity:.1f}%")
except Exception as e:
logger.error(f"Failed to read sensors: {e}")
def _update_status_led(self) -> None:
"""Blink status LED to show activity."""
try:
from gpiozero import LED
led_pin = self.get_gpio_pin('status_led')
led = LED(led_pin)
led.on()
import time
time.sleep(0.1)
led.off()
except ImportError:
# gpiozero not available (development mode)
pass
Step 4: Create Models¶
File: plugins/acme/weather_station/models.py
"""Models for Weather Station plugin."""
from django.db import models
class WeatherReading(models.Model):
"""Stores temperature and humidity readings."""
temperature = models.FloatField(
help_text="Temperature in Celsius"
)
humidity = models.FloatField(
help_text="Humidity percentage (0-100)"
)
timestamp = models.DateTimeField(
auto_now_add=True,
help_text="When the reading was taken"
)
class Meta:
# Important: use app_label matching plugin namespace
app_label = 'acme_weather'
ordering = ['-timestamp']
def __str__(self) -> str:
return f"{self.temperature:.1f}°C, {self.humidity:.1f}% at {self.timestamp}"
@property
def temperature_fahrenheit(self) -> float:
"""Convert Celsius to Fahrenheit."""
return (self.temperature * 9/5) + 32
Step 5: Create Views¶
File: plugins/acme/weather_station/views.py
"""Views for Weather Station plugin."""
from django.views.generic import TemplateView, View
from django.http import JsonResponse
from django.utils import timezone
from datetime import timedelta
from .models import WeatherReading
class WeatherDashboardView(TemplateView):
"""Main dashboard showing current weather."""
template_name = 'weather/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get latest reading
latest = WeatherReading.objects.first()
context['latest'] = latest
# Get readings from last 24 hours
day_ago = timezone.now() - timedelta(hours=24)
context['readings'] = WeatherReading.objects.filter(
timestamp__gte=day_ago
)[:50]
# Calculate statistics
if context['readings']:
temps = [r.temperature for r in context['readings']]
context['stats'] = {
'temp_min': min(temps),
'temp_max': max(temps),
'temp_avg': sum(temps) / len(temps),
}
return context
class WeatherAPIView(View):
"""API endpoint for current weather data."""
def get(self, request):
"""Return current weather as JSON."""
latest = WeatherReading.objects.first()
if latest:
data = {
'temperature': round(latest.temperature, 1),
'humidity': round(latest.humidity, 1),
'timestamp': latest.timestamp.isoformat(),
'unit': 'celsius'
}
else:
data = {'error': 'No readings available'}
return JsonResponse(data)
Step 6: Create URLs¶
File: plugins/acme/weather_station/urls.py
"""URL configuration for Weather Station plugin."""
from django.urls import path
from . import views
app_name = 'weather_station'
urlpatterns = [
path('', views.WeatherDashboardView.as_view(), name='dashboard'),
path('api/current/', views.WeatherAPIView.as_view(), name='api_current'),
]
Step 7: Create Templates¶
File: plugins/acme/weather_station/templates/weather/dashboard.html
{% extends 'base.html' %}
{% load static %}
{% block title %}Weather Station - Tinko{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">Weather Station</h1>
<!-- Current Conditions -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title">Temperature</h2>
{% if latest %}
<p class="text-4xl font-bold">{{ latest.temperature|floatformat:1 }}°C</p>
<p class="text-xl">{{ latest.temperature_fahrenheit|floatformat:1 }}°F</p>
{% else %}
<p class="text-gray-500">No data available</p>
{% endif %}
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title">Humidity</h2>
{% if latest %}
<p class="text-4xl font-bold">{{ latest.humidity|floatformat:1 }}%</p>
{% else %}
<p class="text-gray-500">No data available</p>
{% endif %}
</div>
</div>
</div>
<!-- Statistics -->
{% if stats %}
<div class="card bg-base-200 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title">24-Hour Statistics</h2>
<div class="stats shadow">
<div class="stat">
<div class="stat-title">Min Temp</div>
<div class="stat-value">{{ stats.temp_min|floatformat:1 }}°C</div>
</div>
<div class="stat">
<div class="stat-title">Max Temp</div>
<div class="stat-value">{{ stats.temp_max|floatformat:1 }}°C</div>
</div>
<div class="stat">
<div class="stat-title">Avg Temp</div>
<div class="stat-value">{{ stats.temp_avg|floatformat:1 }}°C</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Historical Data -->
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title">Recent Readings</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Time</th>
<th>Temperature</th>
<th>Humidity</th>
</tr>
</thead>
<tbody>
{% for reading in readings %}
<tr>
<td>{{ reading.timestamp|date:"H:i" }}</td>
<td>{{ reading.temperature|floatformat:1 }}°C</td>
<td>{{ reading.humidity|floatformat:1 }}%</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-gray-500">
No readings available
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
Step 8: Run Migrations¶
After creating the plugin, run migrations to create database tables:
Step 9: Test the Plugin¶
- Restart the Django development server
- The plugin should be automatically discovered
- Visit:
http://localhost:8000/plugins/acme/weather_station/ - You should see the weather dashboard
How It Works¶
Auto-Discovery¶
Tinko automatically discovers plugins from the plugins/ directory:
- Scans
plugins/*/*/ - Finds directories with
__init__.pyandplugin.py - Registers the plugin class
- Adds to Django's
INSTALLED_APPS
Lifecycle¶
- boot(): Called when plugin loads - register GPIO, schedule tasks
- register(): Called after boot - register models, URLs, settings
- uninstall(): Called when disabled - cleanup GPIO, resources
Namespace¶
Plugin is identified as: acme.weather_station
- URL:
/plugins/acme/weather_station/ - Admin: Shows in plugin list
- Settings: Stored as
acme.weather_station.*
Next Steps¶
Now you have a working plugin! You can:
- Add more sensors (pressure, light)
- Create charts with JavaScript
- Add WebSocket for real-time updates
- Implement user authentication
- Add data export features
See Plugin API Reference for complete API documentation.
Common Issues¶
"Plugin not found"¶
- Check
__init__.pyexists - Verify
Pluginclass inherits fromPluginBase - Restart Django server
"Models not created"¶
- Run
makemigrations - Check
app_labelmatches namespace - Verify model registration in
register()
"GPIO not working"¶
- Check pin numbers (BCM numbering)
- Verify user in
gpiogroup - Use mock mode for development
Best Practices¶
- Always cleanup GPIO in
uninstall() - Use type hints for better code
- Log errors with context
- Handle exceptions gracefully
- Document your plugin
Congratulations on creating your first Tinko plugin! 🎉