diff --git a/app/controllers/ui/devices_controller.rb b/app/controllers/ui/devices_controller.rb
index cbc9e3a8e..1a99d83b8 100644
--- a/app/controllers/ui/devices_controller.rb
+++ b/app/controllers/ui/devices_controller.rb
@@ -164,6 +164,7 @@ def device_params
:notify_stopped_publishing,
:hardware_version_override,
:mac_address,
+ :forwarding_destination_id,
{ :tag_ids => [] },
{ :postprocessing_attributes => :hardware_url },
)
diff --git a/app/controllers/v0/devices_controller.rb b/app/controllers/v0/devices_controller.rb
index 3a771be54..331800651 100644
--- a/app/controllers/v0/devices_controller.rb
+++ b/app/controllers/v0/devices_controller.rb
@@ -106,8 +106,19 @@ def device_params
# Researchers + Admins can update is_test and enable_forwarding
if current_user.is_admin_or_researcher?
- params_to_permit.push({postprocessing_attributes: [:blueprint_url, :hardware_url, :latest_postprocessing, :meta, :forwarding_params]})
+ params_to_permit.push(
+ {
+ postprocessing_attributes: [
+ :blueprint_url,
+ :hardware_url,
+ :latest_postprocessing,
+ :meta,
+ :forwarding_params
+ ]
+ }
+ )
params_to_permit.push(:enable_forwarding)
+ params_to_permit.push(:forwarding_destination_id)
end
if current_user.is_admin?
diff --git a/app/lib/presenters/device_presenter.rb b/app/lib/presenters/device_presenter.rb
index 27565b53c..b2550c932 100644
--- a/app/lib/presenters/device_presenter.rb
+++ b/app/lib/presenters/device_presenter.rb
@@ -43,8 +43,9 @@ def data_policy
{
is_private: device.is_private,
enable_forwarding: device.enable_forwarding,
- precise_location: device.precise_location
- }
+ precise_location: device.precise_location,
+ forwarding_destination: device.forwarding_destination&.name
+ }.compact
end
end
diff --git a/app/models/device.rb b/app/models/device.rb
index efa9f83be..9071635f2 100644
--- a/app/models/device.rb
+++ b/app/models/device.rb
@@ -22,6 +22,7 @@ class Device < ActiveRecord::Base
multisearchable :against => [:name, :description, :city, :country_name], if: :active?
belongs_to :owner, class_name: 'User'
+ belongs_to :forwarding_destination, optional: true
has_many :devices_tags, dependent: :destroy
has_many :tags, through: :devices_tags
@@ -117,7 +118,7 @@ def self.ransackable_associations(auth_object = nil)
[
"components", "devices_tags", "owner",
"pg_search_document" , "postprocessing", "sensors",
- "tags"
+ "tags", "forwarding_destination"
]
end
@@ -296,7 +297,8 @@ def data_policy(authorized=false)
{
is_private: is_private,
enable_forwarding: authorized ? enable_forwarding : "[FILTERED]",
- precise_location: authorized ? precise_location : "[FILTERED]"
+ precise_location: authorized ? precise_location : "[FILTERED]",
+ forwarding_destination: authorized ? forwarding_destination&.name : "[FILTERED]"
}
end
diff --git a/app/models/forwarding_destination.rb b/app/models/forwarding_destination.rb
new file mode 100644
index 000000000..f677fd1cd
--- /dev/null
+++ b/app/models/forwarding_destination.rb
@@ -0,0 +1,8 @@
+class ForwardingDestination < ActiveRecord::Base
+ has_many :devices
+ validates_uniqueness_of :name
+
+ def self.ransackable_attributes(_auth_object = nil)
+ ["name"]
+ end
+end
diff --git a/app/views/ui/devices/_fields.html.erb b/app/views/ui/devices/_fields.html.erb
index d7a338308..a844073d1 100644
--- a/app/views/ui/devices/_fields.html.erb
+++ b/app/views/ui/devices/_fields.html.erb
@@ -48,6 +48,10 @@
<%= t(:device_form_researcher_options_subhead) %>
<% if device.owner.forward_device_readings? %>
<%= form.check_box :enable_forwarding, label: t(:device_form_enable_forwarding_label) %>
+ <%= form.select :forwarding_destination_id, options_for_select(
+ ForwardingDestination.all.map { |fd| [fd.name, fd.id] },
+ device.forwarding_destination_id
+ ), label: t(:device_form_forwarding_destination_label), include_blank: "Default" %>
<% end %>
<%= form.fields_for :postprocessing, device.postprocessing || Postprocessing.new do |fp| %>
<%= fp.text_field :hardware_url, label: t(:device_form_hardware_url_label), help: t(:device_form_postprocessing_blurb_html) %>
diff --git a/config/locales/views/devices/en.yml b/config/locales/views/devices/en.yml
index 7e10966cb..745a4bae1 100644
--- a/config/locales/views/devices/en.yml
+++ b/config/locales/views/devices/en.yml
@@ -26,6 +26,7 @@ en:
device_form_is_private_label: "Make this kit private"
device_form_precise_location_label: "Enable precise location"
device_form_enable_forwarding_label: "Enable MQTT forwarding"
+ device_form_forwarding_destination_label: "Destination for MQTT forwarding"
device_form_notifications_subhead: "Notifications"
device_form_notifications_blurb: "Get emails when the following events occur:"
device_form_notify_low_battery_label: "Battery level drops below 15%"
diff --git a/db/migrate/20250715180503_add_forwarding_destinations.rb b/db/migrate/20250715180503_add_forwarding_destinations.rb
new file mode 100644
index 000000000..8c328eb4f
--- /dev/null
+++ b/db/migrate/20250715180503_add_forwarding_destinations.rb
@@ -0,0 +1,14 @@
+class AddForwardingDestinations < ActiveRecord::Migration[6.1]
+ def change
+ create_table :forwarding_destinations do |t|
+ t.string :name, null: false
+ t.timestamps
+ end
+
+ add_index :forwarding_destinations, :name, unique: true
+
+ change_table :devices do |t|
+ t.belongs_to :forwarding_destination, index: true, null: true, foreign_key: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 097d22cd4..90b413b01 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2025_05_05_081245) do
+ActiveRecord::Schema.define(version: 2025_07_15_180503) do
# These are extensions that must be enabled in order to support this database
enable_extension "adminpack"
@@ -109,7 +109,9 @@
t.string "hardware_slug_override"
t.boolean "precise_location", default: true, null: false
t.boolean "enable_forwarding", default: false, null: false
+ t.bigint "forwarding_destination_id"
t.index ["device_token"], name: "index_devices_on_device_token", unique: true
+ t.index ["forwarding_destination_id"], name: "index_devices_on_forwarding_destination_id"
t.index ["geohash"], name: "index_devices_on_geohash"
t.index ["last_reading_at"], name: "index_devices_on_last_reading_at"
t.index ["owner_id"], name: "index_devices_on_owner_id"
@@ -150,6 +152,13 @@
t.index ["owner_id"], name: "index_experiments_on_owner_id"
end
+ create_table "forwarding_destinations", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["name"], name: "index_forwarding_destinations_on_name", unique: true
+ end
+
create_table "friendly_id_slugs", id: :serial, force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
@@ -346,6 +355,7 @@
add_foreign_key "api_tokens", "users", column: "owner_id"
add_foreign_key "components", "devices"
add_foreign_key "components", "sensors"
+ add_foreign_key "devices", "forwarding_destinations"
add_foreign_key "devices_experiments", "devices"
add_foreign_key "devices_experiments", "experiments"
add_foreign_key "devices_tags", "devices"