diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/app/Application.js | 7 | ||||
-rw-r--r-- | web/app/controller/Root.js | 7 | ||||
-rw-r--r-- | web/app/model/Device.js | 2 | ||||
-rw-r--r-- | web/app/model/Event.js | 3 | ||||
-rw-r--r-- | web/app/model/Geofence.js | 36 | ||||
-rw-r--r-- | web/app/store/AllGeofences.js | 28 | ||||
-rw-r--r-- | web/app/store/Geofences.js | 28 | ||||
-rw-r--r-- | web/app/view/DeviceGeofences.js | 43 | ||||
-rw-r--r-- | web/app/view/DeviceGeofencesController.js | 80 | ||||
-rw-r--r-- | web/app/view/Devices.js | 8 | ||||
-rw-r--r-- | web/app/view/DevicesController.js | 16 | ||||
-rw-r--r-- | web/app/view/GroupGeofences.js | 43 | ||||
-rw-r--r-- | web/app/view/GroupGeofencesController.js | 80 | ||||
-rw-r--r-- | web/app/view/Groups.js | 11 | ||||
-rw-r--r-- | web/app/view/GroupsController.js | 16 | ||||
-rw-r--r-- | web/app/view/UserGeofences.js | 44 | ||||
-rw-r--r-- | web/app/view/UserGeofencesController.js | 79 | ||||
-rw-r--r-- | web/app/view/Users.js | 5 | ||||
-rw-r--r-- | web/app/view/UsersController.js | 13 | ||||
-rw-r--r-- | web/l10n/en.json | 2 |
20 files changed, 547 insertions, 4 deletions
diff --git a/web/app/Application.js b/web/app/Application.js index e798a73e2..dbbc7a594 100644 --- a/web/app/Application.js +++ b/web/app/Application.js @@ -31,7 +31,8 @@ Ext.define('Traccar.Application', { 'Position', 'Attribute', 'Command', - 'Event' + 'Event', + 'Geofence' ], stores: [ @@ -49,7 +50,9 @@ Ext.define('Traccar.Application', { 'CommandTypes', 'TimeUnits', 'Languages', - 'Events' + 'Events', + 'Geofences', + 'AllGeofences' ], controllers: [ diff --git a/web/app/controller/Root.js b/web/app/controller/Root.js index 991a2572c..bbbe616a0 100644 --- a/web/app/controller/Root.js +++ b/web/app/controller/Root.js @@ -75,6 +75,7 @@ Ext.define('Traccar.controller.Root', { loadApp: function () { Ext.getStore('Groups').load(); Ext.getStore('Devices').load(); + Ext.getStore('Geofences').load(); Ext.get('attribution').remove(); if (this.isPhone) { Ext.create('widget.mainMobile'); @@ -147,6 +148,12 @@ Ext.define('Traccar.controller.Root', { text = typeKey; } } + if (array[i].geofenceId !== 0) { + geofence = Ext.getStore('Geofences').getById(array[i].geofenceId); + if (typeof geofence != "undefined") { + text += ' \"' + geofence.getData().name + '"'; + } + } device = Ext.getStore('Devices').getById(array[i].deviceId); if (typeof device != "undefined") { Ext.toast(text, device.getData().name); diff --git a/web/app/model/Device.js b/web/app/model/Device.js index 588d53c1f..c0a6739f5 100644 --- a/web/app/model/Device.js +++ b/web/app/model/Device.js @@ -37,5 +37,7 @@ Ext.define('Traccar.model.Device', { }, { name: 'groupId', type: 'int' + }, { + name: 'geofenceIds', }] }); diff --git a/web/app/model/Event.js b/web/app/model/Event.js index 4dd3ea7ff..698ebb535 100644 --- a/web/app/model/Event.js +++ b/web/app/model/Event.js @@ -35,6 +35,9 @@ Ext.define('Traccar.model.Event', { name: 'positionId', type: 'int' }, { + name: 'geofenceId', + type: 'int' + }, { name: 'attributes' }] }); diff --git a/web/app/model/Geofence.js b/web/app/model/Geofence.js new file mode 100644 index 000000000..a832455ac --- /dev/null +++ b/web/app/model/Geofence.js @@ -0,0 +1,36 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.model.Geofence', { + extend: 'Ext.data.Model', + identifier: 'negative', + + fields: [{ + name: 'id', + type: 'int' + }, { + name: 'name', + type: 'string' + }, { + name: 'description', + type: 'string' + }, { + name: 'area', + type: 'string' + }, { + name: 'attributes' + }] +}); diff --git a/web/app/store/AllGeofences.js b/web/app/store/AllGeofences.js new file mode 100644 index 000000000..aa6f9abfc --- /dev/null +++ b/web/app/store/AllGeofences.js @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.store.AllGeofences', { + extend: 'Ext.data.Store', + model: 'Traccar.model.Geofence', + + proxy: { + type: 'rest', + url: '/api/geofences', + extraParams: { + all: true + } + } +}); diff --git a/web/app/store/Geofences.js b/web/app/store/Geofences.js new file mode 100644 index 000000000..8c5c0b3cf --- /dev/null +++ b/web/app/store/Geofences.js @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.store.Geofences', { + extend: 'Ext.data.Store', + model: 'Traccar.model.Geofence', + + proxy: { + type: 'rest', + url: '/api/geofences', + writer: { + writeAllFields: true + } + } +}); diff --git a/web/app/view/DeviceGeofences.js b/web/app/view/DeviceGeofences.js new file mode 100644 index 000000000..a309cc2ed --- /dev/null +++ b/web/app/view/DeviceGeofences.js @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.DeviceGeofences', { + extend: 'Ext.grid.Panel', + xtype: 'deviceGeofencesView', + + requires: [ + 'Traccar.view.DeviceGeofencesController' + ], + + controller: 'deviceGeofences', + + selModel: { + selType: 'checkboxmodel', + checkOnly: true, + showHeaderCheckbox: false + }, + + listeners: { + beforedeselect: 'onBeforeDeselect', + beforeselect: 'onBeforeSelect' + }, + + columns: [{ + text: Strings.sharedName, + dataIndex: 'name', + flex: 1 + }] +}); diff --git a/web/app/view/DeviceGeofencesController.js b/web/app/view/DeviceGeofencesController.js new file mode 100644 index 000000000..c9d4b0aa9 --- /dev/null +++ b/web/app/view/DeviceGeofencesController.js @@ -0,0 +1,80 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.DeviceGeofencesController', { + extend: 'Ext.app.ViewController', + alias: 'controller.deviceGeofences', + + init: function () { + var admin = Traccar.app.getUser().get('admin'); + this.deviceId = this.getView().device.getData().id; + this.getView().setStore(Ext.getStore((admin) ? 'AllGeofences' : 'Geofences')); + this.getView().getStore().load({ + scope: this, + callback: function (records, operation, success) { + var deviceStore = Ext.create((admin) ? 'Traccar.store.AllGeofences' : 'Traccar.store.Geofences'); + deviceStore.load({ + params: { + deviceId: this.deviceId + }, + scope: this, + callback: function (records, operation, success) { + var i, index; + if (success) { + for (i = 0; i < records.length; i++) { + index = this.getView().getStore().find('id', records[i].getData().id); + this.getView().getSelectionModel().select(index, true, true); + } + } + } + }); + } + }); + }, + + onBeforeSelect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + url: '/api/devices/geofences', + jsonData: { + deviceId: this.deviceId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + }, + + onBeforeDeselect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + method: 'DELETE', + url: '/api/devices/geofences', + jsonData: { + deviceId: this.deviceId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + } +}); diff --git a/web/app/view/Devices.js b/web/app/view/Devices.js index f06c2658b..1a70dfef8 100644 --- a/web/app/view/Devices.js +++ b/web/app/view/Devices.js @@ -41,6 +41,14 @@ Ext.define('Traccar.view.Devices', { tbar: { xtype: 'editToolbar', items: [{ + xtype: 'button', + disabled: true, + handler: 'onGeofencesClick', + reference: 'toolbarGeofencesButton', + glyph: 'xf21d@FontAwesome', + tooltip: Strings.sharedGeofences, + tooltipType: 'title' + }, { disabled: true, handler: 'onCommandClick', reference: 'deviceCommandButton', diff --git a/web/app/view/DevicesController.js b/web/app/view/DevicesController.js index 6b79a6804..d33368181 100644 --- a/web/app/view/DevicesController.js +++ b/web/app/view/DevicesController.js @@ -20,7 +20,8 @@ Ext.define('Traccar.view.DevicesController', { requires: [ 'Traccar.view.CommandDialog', - 'Traccar.view.DeviceDialog' + 'Traccar.view.DeviceDialog', + 'Traccar.view.DeviceGeofences' ], config: { @@ -84,6 +85,18 @@ Ext.define('Traccar.view.DevicesController', { }); }, + onGeofencesClick: function () { + device = this.getView().getSelectionModel().getSelection()[0]; + var admin = Traccar.app.getUser().get('admin'); + Ext.create('Traccar.view.BaseWindow', { + title: Strings.settingsGeofences, + items: { + xtype: 'deviceGeofencesView', + device: device + } + }).show(); + }, + onCommandClick: function () { var device, deviceId, command, dialog, comboStore; device = this.getView().getSelectionModel().getSelection()[0]; @@ -108,6 +121,7 @@ Ext.define('Traccar.view.DevicesController', { var empty = selected.getCount() === 0; this.lookupReference('toolbarEditButton').setDisabled(empty); this.lookupReference('toolbarRemoveButton').setDisabled(empty); + this.lookupReference('toolbarGeofencesButton').setDisabled(empty); this.lookupReference('deviceCommandButton').setDisabled(empty || (selected.getLastSelected().get('status') !== 'online')); if (!empty) { this.fireEvent('selectDevice', selected.getLastSelected(), true); diff --git a/web/app/view/GroupGeofences.js b/web/app/view/GroupGeofences.js new file mode 100644 index 000000000..9a46f3964 --- /dev/null +++ b/web/app/view/GroupGeofences.js @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.GroupGeofences', { + extend: 'Ext.grid.Panel', + xtype: 'groupGeofencesView', + + requires: [ + 'Traccar.view.GroupGeofencesController' + ], + + controller: 'groupGeofences', + + selModel: { + selType: 'checkboxmodel', + checkOnly: true, + showHeaderCheckbox: false + }, + + listeners: { + beforedeselect: 'onBeforeDeselect', + beforeselect: 'onBeforeSelect' + }, + + columns: [{ + text: Strings.sharedName, + dataIndex: 'name', + flex: 1 + }] +}); diff --git a/web/app/view/GroupGeofencesController.js b/web/app/view/GroupGeofencesController.js new file mode 100644 index 000000000..e8b2a6f52 --- /dev/null +++ b/web/app/view/GroupGeofencesController.js @@ -0,0 +1,80 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.GroupGeofencesController', { + extend: 'Ext.app.ViewController', + alias: 'controller.groupGeofences', + + init: function () { + var admin = Traccar.app.getUser().get('admin'); + this.groupId = this.getView().group.getData().id; + this.getView().setStore(Ext.getStore((admin) ? 'AllGeofences' : 'Geofences')); + this.getView().getStore().load({ + scope: this, + callback: function (records, operation, success) { + var groupStore = Ext.create((admin) ? 'Traccar.store.AllGeofences' : 'Traccar.store.Geofences'); + groupStore.load({ + params: { + groupId: this.groupId + }, + scope: this, + callback: function (records, operation, success) { + var i, index; + if (success) { + for (i = 0; i < records.length; i++) { + index = this.getView().getStore().find('id', records[i].getData().id); + this.getView().getSelectionModel().select(index, true, true); + } + } + } + }); + } + }); + }, + + onBeforeSelect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + url: '/api/groups/geofences', + jsonData: { + groupId: this.groupId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + }, + + onBeforeDeselect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + method: 'DELETE', + url: '/api/groups/geofences', + jsonData: { + groupId: this.groupId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + } +}); diff --git a/web/app/view/Groups.js b/web/app/view/Groups.js index 8404c59a9..59d20df31 100644 --- a/web/app/view/Groups.js +++ b/web/app/view/Groups.js @@ -29,7 +29,16 @@ Ext.define('Traccar.view.Groups', { selType: 'rowmodel', tbar: { - xtype: 'editToolbar' + xtype: 'editToolbar', + items: [{ + xtype: 'button', + disabled: true, + handler: 'onGeofencesClick', + reference: 'toolbarGeofencesButton', + glyph: 'xf21d@FontAwesome', + tooltip: Strings.sharedGeofences, + tooltipType: 'title' + }] }, listeners: { diff --git a/web/app/view/GroupsController.js b/web/app/view/GroupsController.js index 6cc568ea2..396a28e7e 100644 --- a/web/app/view/GroupsController.js +++ b/web/app/view/GroupsController.js @@ -18,6 +18,10 @@ Ext.define('Traccar.view.GroupsController', { extend: 'Ext.app.ViewController', alias: 'controller.groups', + requires: [ + 'Traccar.view.GroupGeofences' + ], + onAddClick: function () { var group, dialog; group = Ext.create('Traccar.model.Group'); @@ -55,9 +59,21 @@ Ext.define('Traccar.view.GroupsController', { }); }, + onGeofencesClick: function () { + group = this.getView().getSelectionModel().getSelection()[0]; + Ext.create('Traccar.view.BaseWindow', { + title: Strings.settingsGeofences, + items: { + xtype: 'groupGeofencesView', + group: group + } + }).show(); + }, + onSelectionChange: function (selected) { var disabled = selected.length > 0; this.lookupReference('toolbarEditButton').setDisabled(disabled); this.lookupReference('toolbarRemoveButton').setDisabled(disabled); + this.lookupReference('toolbarGeofencesButton').setDisabled(disabled); } }); diff --git a/web/app/view/UserGeofences.js b/web/app/view/UserGeofences.js new file mode 100644 index 000000000..83ded6665 --- /dev/null +++ b/web/app/view/UserGeofences.js @@ -0,0 +1,44 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.UserGeofences', { + extend: 'Ext.grid.Panel', + xtype: 'userGeofencesView', + + requires: [ + 'Traccar.view.UserGeofencesController' + ], + + controller: 'userGeofences', + store: 'AllGeofences', + + selModel: { + selType: 'checkboxmodel', + checkOnly: true, + showHeaderCheckbox: false + }, + + listeners: { + beforedeselect: 'onBeforeDeselect', + beforeselect: 'onBeforeSelect' + }, + + columns: [{ + text: Strings.sharedName, + dataIndex: 'name', + flex: 1 + }] +}); diff --git a/web/app/view/UserGeofencesController.js b/web/app/view/UserGeofencesController.js new file mode 100644 index 000000000..5ce13b51e --- /dev/null +++ b/web/app/view/UserGeofencesController.js @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.define('Traccar.view.UserGeofencesController', { + extend: 'Ext.app.ViewController', + alias: 'controller.userGeofences', + + init: function () { + this.userId = this.getView().user.getData().id; + this.getView().getStore().load({ + scope: this, + callback: function (records, operation, success) { + var userStore = Ext.create('Traccar.store.Geofences'); + + userStore.load({ + params: { + userId: this.userId + }, + scope: this, + callback: function (records, operation, success) { + var i, index; + if (success) { + for (i = 0; i < records.length; i++) { + index = this.getView().getStore().find('id', records[i].getData().id); + this.getView().getSelectionModel().select(index, true, true); + } + } + } + }); + } + }); + }, + + onBeforeSelect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + url: '/api/permissions/geofences', + jsonData: { + userId: this.userId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + }, + + onBeforeDeselect: function (object, record, index) { + Ext.Ajax.request({ + scope: this, + method: 'DELETE', + url: '/api/permissions/geofences', + jsonData: { + userId: this.userId, + geofenceId: record.getData().id + }, + callback: function (options, success, response) { + if (!success) { + Traccar.app.showError(response); + } + } + }); + } +}); diff --git a/web/app/view/Users.js b/web/app/view/Users.js index 408a70885..4c5b2a05b 100644 --- a/web/app/view/Users.js +++ b/web/app/view/Users.js @@ -40,6 +40,11 @@ Ext.define('Traccar.view.Users', { disabled: true, handler: 'onGroupsClick', reference: 'userGroupsButton' + }, { + text: Strings.settingsGeofences, + disabled: true, + handler: 'onGeofencesClick', + reference: 'userGeofencesButton' }] }, diff --git a/web/app/view/UsersController.js b/web/app/view/UsersController.js index c48f57cf4..acf718c82 100644 --- a/web/app/view/UsersController.js +++ b/web/app/view/UsersController.js @@ -22,6 +22,7 @@ Ext.define('Traccar.view.UsersController', { 'Traccar.view.UserDialog', 'Traccar.view.UserDevices', 'Traccar.view.UserGroups', + 'Traccar.view.UserGeofences', 'Traccar.view.BaseWindow' ], @@ -87,11 +88,23 @@ Ext.define('Traccar.view.UsersController', { }).show(); }, + onGeofencesClick: function () { + var user = this.getView().getSelectionModel().getSelection()[0]; + Ext.create('Traccar.view.BaseWindow', { + title: Strings.settingsGeofences, + items: { + xtype: 'userGeofencesView', + user: user + } + }).show(); + }, + onSelectionChange: function (selected) { var disabled = selected.length > 0; this.lookupReference('toolbarEditButton').setDisabled(disabled); this.lookupReference('toolbarRemoveButton').setDisabled(disabled); this.lookupReference('userDevicesButton').setDisabled(disabled); this.lookupReference('userGroupsButton').setDisabled(disabled); + this.lookupReference('userGeofencesButton').setDisabled(disabled); } }); diff --git a/web/l10n/en.json b/web/l10n/en.json index 48dc6533c..4455071d2 100644 --- a/web/l10n/en.json +++ b/web/l10n/en.json @@ -16,6 +16,7 @@ "sharedSecond": "Second", "sharedName": "Name", "sharedSearch": "Search", + "sharedGeofences": "Geofences", "errorTitle": "Error", "errorUnknown": "Unknown error", "errorConnection": "Connection error", @@ -47,6 +48,7 @@ "settingsDistanceUnit": "Distance", "settingsSpeedUnit": "Speed", "settingsTwelveHourFormat": "12-hour Format", + "settingsGeofences": "Geofences", "reportTitle": "Reports", "reportDevice": "Device", "reportFrom": "From", |