Adding a Feature¶
Most new features in OpenCrate follow the same implementation path. This guide walks through each step with concrete file paths and wiring instructions.
Step-by-step¶
1. Create the store¶
If your feature needs persistent state, start with a store.
Create the file: src/store/my_feature_store.rs
Follow the standard store pattern (see Developer Guide):
- Define a command enum with all operations, each carrying a
oneshot::Senderfor the reply. - Define your public struct with an
mpsc::Sender<Cmd>field. - In the constructor, spawn a
std::threadthat opens a SQLite connection atdata/my_feature.db(WAL mode), runs migrations, and loops onrx.blocking_recv(). - Accept
Option<EventBus>in the constructor. Publish relevant events after successful mutations. UseNonein tests. - Expose async methods that send commands and await oneshot replies.
Register the module: Add pub mod my_feature_store; to src/store/mod.rs.
2. Wire into the platform¶
Open src/platform.rs and add your store to the initialization sequence.
-
Create the store in
init_platform(): -
Add to
AutomationState(orModelStateif it is core model data): -
Add to
SharedPlatformif the API needs access:
3. Add API routes¶
If your feature needs HTTP endpoints, create a route module.
Create the file: src/api/routes/my_feature.rs
use axum::{extract::State, Json, Router, routing::{get, post}};
use crate::api::ApiState;
pub fn routes() -> Router<ApiState> {
Router::new()
.route("/api/my-feature", get(list_items).post(create_item))
.route("/api/my-feature/:id", get(get_item).put(update_item).delete(delete_item))
}
async fn list_items(State(state): State<ApiState>) -> Json<Vec<MyItem>> {
let items = state.my_feature_store.list().await;
Json(items)
}
// ... other handlers
Register the routes: In src/api/routes/mod.rs, declare the module and merge the router:
pub mod my_feature;
pub fn all_routes() -> Router<ApiState> {
Router::new()
// ... existing routes
.merge(my_feature::routes())
}
Add to ApiState: In src/api/mod.rs, add your store to the ApiState struct and populate it from SharedPlatform.
4. Add permissions¶
If your feature needs access control, add a permission variant.
In src/auth.rs:
Gate your API handlers with the permission check. Add audit action variants for mutations (Create, Update, Delete operations).
5. Add GUI components¶
If your feature needs a user interface, create Dioxus components.
Create the file: src/gui/components/my_feature_view.rs
use dioxus::prelude::*;
use crate::gui::state::AppState;
#[component]
pub fn MyFeatureView(cx: Scope) -> Element {
let state = use_shared_state::<AppState>(cx)?;
// Component implementation
}
Register the component: Add pub mod my_feature_view; to src/gui/components/mod.rs.
Add to navigation: Wire the component into the appropriate tab in the GUI. Most features go under the Config section as a new tab, or under an existing view as a sub-tab.
Add to AppState: If your component needs state beyond what the store provides, add fields to src/gui/state.rs.
6. Add event subscribers¶
If your feature needs to react to system events, create an EventBus subscriber.
pub struct MyFeatureEngine {
// ...
}
impl MyFeatureEngine {
pub fn start(event_bus: &EventBus, store: MyFeatureStore) {
let mut rx = event_bus.subscribe();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
match event.as_ref() {
Event::AlarmRaised { .. } => {
// React to alarms
}
Event::ValueChanged { .. } => {
// React to value changes
}
_ => {}
}
}
});
}
}
Start the engine in init_platform() after creating all stores:
7. Add to lib.rs¶
Declare your top-level module in src/lib.rs if you created a new module directory:
Checklist¶
Use this as a quick reference when adding a feature:
- [ ] Store:
src/store/my_feature_store.rs+ register insrc/store/mod.rs - [ ] Platform: wire store in
src/platform.rs(create instance, add toAutomationState/SharedPlatform) - [ ] API routes:
src/api/routes/my_feature.rs+ register insrc/api/routes/mod.rs+ add store toApiState - [ ] Auth: permission variant in
src/auth.rs+ audit action variants - [ ] GUI component:
src/gui/components/my_feature_view.rs+ register insrc/gui/components/mod.rs - [ ] GUI state: add fields to
src/gui/state.rsif needed - [ ] Event subscriber: create engine/subscriber + start in
init_platform() - [ ] Module declaration: add
pub modtosrc/lib.rsif new top-level module - [ ] Tests: store tests with
NoneEventBus and:memory:database
Examples from the codebase¶
These existing features followed exactly this pattern and serve as good references:
| Feature | Store | API routes | GUI component | Event subscriber |
|---|---|---|---|---|
| MQTT | mqtt_store.rs |
(in routes/mod.rs) | Config > MQTT tab | MqttPublisher |
| Webhooks | webhook_store.rs |
routes/webhooks.rs |
webhook_settings.rs |
WebhookDispatcher |
| Reporting | report_store.rs |
routes/ (reports) |
Config > Reports tab | Report scheduler |
| Energy | energy_store.rs |
routes/energy.rs |
energy_view.rs |
Energy scheduler |
| Commissioning | commissioning_store.rs |
-- | commissioning_tab.rs |
-- |
| Notifications | notification_store.rs |
-- | Config > Alarm Routing tab | AlarmRouter |
Each of these follows the same store-then-platform-then-API-then-GUI progression described above.