Plugin Best Practices¶
Guidelines for creating high-quality, maintainable Tinko plugins.
Code Organization¶
Directory Structure¶
plugins/author/pluginname/
├── __init__.py # Required
├── plugin.py # Required
├── models.py # Optional
├── views.py # Optional
├── urls.py # Optional
├── forms.py # Optional
├── consumers.py # Optional (WebSocket)
├── services.py # Optional (business logic)
├── utils.py # Optional (helpers)
├── static/ # Optional
│ ├── css/
│ └── js/
├── templates/ # Optional
│ └── pluginname/
└── tests.py # Recommended
Separation of Concerns¶
Keep your code organized:
- plugin.py: Registration, lifecycle
- models.py: Data models
- views.py: HTTP request handling
- services.py: Business logic
- utils.py: Helper functions
- tests.py: Unit tests
GPIO Safety¶
Always Cleanup¶
def uninstall(self):
"""Always cleanup GPIO pins."""
self.cleanup_gpio_pins()
# Additional cleanup...
Validate Pins¶
def boot(self):
# Pin numbers are validated by register_gpio_pins
self.register_gpio_pins({
'led': 17, # Valid: 0-27
'sensor': 4,
})
Use Context Managers¶
from gpiozero import LED
def blink_led(self):
pin = self.get_gpio_pin('led')
with LED(pin) as led:
led.blink()
Mock for Development¶
try:
from gpiozero import LED
except ImportError:
from unittest.mock import MagicMock
LED = MagicMock
Error Handling¶
Graceful Degradation¶
def read_sensor(self):
try:
value = self._hardware.read()
return value
except HardwareError as e:
logger.error(f"Sensor error: {e}")
return None # Graceful fallback
Don't Crash the System¶
def boot(self):
try:
self.initialize_hardware()
except Exception as e:
logger.error(f"Hardware init failed: {e}")
# Plugin loads but without hardware
Log with Context¶
import logging
logger = logging.getLogger(__name__)
def process_data(self, data):
logger.info(f"Processing {len(data)} records from {self.name}")
# ...
Database Models¶
Proper App Labels¶
Model Organization¶
# models.py
class Reading(models.Model):
"""Single sensor reading."""
value = models.FloatField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'acme_weather'
ordering = ['-timestamp']
class Config(models.Model):
"""Plugin configuration."""
key = models.CharField(max_length=50)
value = models.TextField()
class Meta:
app_label = 'acme_weather'
URL Patterns¶
URL Namespacing¶
# urls.py
app_name = 'weather_station'
urlpatterns = [
path('', views.Dashboard.as_view(), name='dashboard'),
path('api/', views.API.as_view(), name='api'),
]
Reverse URLs¶
Templates¶
Template Naming¶
# Good: Namespaced templates
return render(request, 'weather/dashboard.html')
# Bad: Generic names
return render(request, 'dashboard.html')
Template Structure¶
<!-- templates/weather/dashboard.html -->
{% extends 'base.html' %}
{% load static %}
{% block title %}Weather - {{ block.super }}{% endblock %}
{% block content %}
<div class="container">
<h1>Weather Station</h1>
<!-- content -->
</div>
{% endblock %}
Static Files¶
Organization¶
Referencing¶
{% load static %}
<link rel="stylesheet" href="{% static 'weather/css/style.css' %}">
<script src="{% static 'weather/js/dashboard.js' %}"></script>
Testing¶
Test Structure¶
# tests.py
from django.test import TestCase
from core.plugin_system.base import plugin_manager
class WeatherPluginTests(TestCase):
def setUp(self):
self.plugin = plugin_manager.get_plugin('acme.weather')
def test_plugin_loaded(self):
self.assertIsNotNone(self.plugin)
self.assertEqual(self.plugin.name, "Weather Station")
def test_gpio_pins_registered(self):
pin = self.plugin.get_gpio_pin('sensor')
self.assertEqual(pin, 4)
Mock GPIO¶
from unittest.mock import patch, MagicMock
@patch('plugins.acme.weather.plugin.LED')
def test_led_blink(self, mock_led):
self.plugin.blink_status()
mock_led.return_value.blink.assert_called_once()
Type Hints¶
Use Type Hints¶
from typing import Optional, Dict, Any
def process_data(self, data: Dict[str, Any]) -> Optional[float]:
"""Process sensor data.
Args:
data: Raw sensor readings
Returns:
Processed value or None if invalid
"""
if not data:
return None
return float(data.get('value', 0))
Documentation¶
Docstrings¶
def read_sensor(self) -> float:
"""Read temperature from sensor.
Reads the DHT22 sensor connected to GPIO pin 4.
Returns:
Temperature in Celsius
Raises:
SensorError: If sensor is not responding
"""
# Implementation...
README¶
Create a README.md in your plugin:
# Weather Station Plugin
Monitor temperature and humidity.
## Features
- Temperature reading
- Humidity reading
- Web dashboard
- Historical data
## Hardware
- DHT22 sensor
- GPIO 4 (data)
- GPIO 17 (status LED)
## Installation
1. Copy to `plugins/acme/weather/`
2. Run migrations
3. Enable in admin
## Configuration
- Update interval: 60 seconds
- Temperature unit: Celsius
Performance¶
Database Queries¶
# Bad: N+1 queries
for reading in Reading.objects.all():
print(reading.plugin.name) # Extra query each time
# Good: Use select_related
for reading in Reading.objects.select_related('plugin'):
print(reading.plugin.name)
Background Tasks¶
# Don't block the main thread
def boot(self):
self.register_schedule(
interval=60,
callback=self.update,
name='background_update'
)
def update(self):
# Runs in background thread
# Can be long-running
pass
Security¶
Input Validation¶
def handle_request(self, request):
pin = request.POST.get('pin')
# Validate pin number
try:
pin = int(pin)
if not 0 <= pin <= 27:
raise ValueError
except ValueError:
return HttpResponseBadRequest("Invalid pin")
Don't Trust User Input¶
def configure(self, settings):
# Validate settings before applying
if settings.get('interval', 0) < 10:
raise ValueError("Interval too short")
Version Management¶
Semantic Versioning¶
1.0.0- Initial release1.1.0- New feature (backward compatible)1.1.1- Bug fix2.0.0- Breaking change
Changelog¶
# Changelog
## 1.1.0 (2024-01-15)
### Added
- Support for DHT11 sensor
- Historical graph view
### Fixed
- GPIO cleanup on shutdown
Debugging¶
Logging Levels¶
logger.debug("Detailed info for debugging")
logger.info("General information")
logger.warning("Warning, but not error")
logger.error("Error occurred")
logger.critical("Critical failure")
Debug Mode¶
def some_method(self):
if self.get_config('debug', False):
# Extra logging
logger.debug(f"Debug: {detailed_info}")
Summary¶
- ✅ Always cleanup GPIO
- ✅ Handle errors gracefully
- ✅ Use type hints
- ✅ Write tests
- ✅ Document your code
- ✅ Follow naming conventions
- ✅ Validate user input
- ✅ Log important events