diff --git a/web/data.json b/web/data.json new file mode 100644 index 00000000..9560a000 --- /dev/null +++ b/web/data.json @@ -0,0 +1,5 @@ +[ + { + "test": "data" + } +] \ No newline at end of file diff --git a/web/init/static/csv/.~lock.nbiotDevice.csv# b/web/init/static/csv/.~lock.nbiotDevice.csv# deleted file mode 100644 index 0c6b14ca..00000000 --- a/web/init/static/csv/.~lock.nbiotDevice.csv# +++ /dev/null @@ -1 +0,0 @@ -,key,devbian,13.08.2024 18:20,file:///home/key/.config/libreoffice/4; \ No newline at end of file diff --git a/web/init/static/csv/nbiotDevice.csv b/web/init/static/csv/nbiotDevice.csv index d9fe2750..e45ca2a1 100644 --- a/web/init/static/csv/nbiotDevice.csv +++ b/web/init/static/csv/nbiotDevice.csv @@ -1,2 +1,2 @@ -name,device_id,serial_no,imsi,iccid,ip,port,user_id,owner_id,manufacturer_id,status_id,type_id,area_id,company_id +name,device_id,serial_no,imsi,iccid,ip,port,device_user_id,device_owner_id,company_manufacturer_id,status_id,type_id,area_id,company_owner_id miniUni,24070580,24070576,901405710203483,898822800001074,10.128.24.42,50000,1,1,1,1,1,1,3 diff --git a/web/instance/test.db b/web/instance/test.db index 7e58bf9d..ebe7fd69 100644 Binary files a/web/instance/test.db and b/web/instance/test.db differ diff --git a/web/minibase/__pycache__/theme.cpython-311.pyc b/web/minibase/__pycache__/theme.cpython-311.pyc index 768a2b05..95a4fc4f 100644 Binary files a/web/minibase/__pycache__/theme.cpython-311.pyc and b/web/minibase/__pycache__/theme.cpython-311.pyc differ diff --git a/web/minibase/blueprints/company/__pycache__/models.cpython-311.pyc b/web/minibase/blueprints/company/__pycache__/models.cpython-311.pyc index c052e6d4..3aea7880 100644 Binary files a/web/minibase/blueprints/company/__pycache__/models.cpython-311.pyc and b/web/minibase/blueprints/company/__pycache__/models.cpython-311.pyc differ diff --git a/web/minibase/blueprints/company/__pycache__/routes.cpython-311.pyc b/web/minibase/blueprints/company/__pycache__/routes.cpython-311.pyc index 6732eb0b..b8f5e271 100644 Binary files a/web/minibase/blueprints/company/__pycache__/routes.cpython-311.pyc and b/web/minibase/blueprints/company/__pycache__/routes.cpython-311.pyc differ diff --git a/web/minibase/blueprints/company/models.py b/web/minibase/blueprints/company/models.py index c167fb4a..06559e56 100644 --- a/web/minibase/blueprints/company/models.py +++ b/web/minibase/blueprints/company/models.py @@ -26,10 +26,14 @@ class Companies(db.Model): country_id = db.Column(db.Integer, db.ForeignKey('countries.id'), nullable=False) state_id = db.Column(db.Integer, db.ForeignKey('states.id'), nullable=False) city_id = db.Column(db.Integer, db.ForeignKey('cities.id'), nullable=False) - + # Many To one notes = db.relationship('Notes', backref='company_notes', lazy='dynamic') + # Relationships to Device + manufactured_devices = db.relationship("nbiotDevice", foreign_keys="nbiotDevice.company_manufacturer_id", back_populates="company_manufacturer") + owned_devices = db.relationship("nbiotDevice", foreign_keys="nbiotDevice.company_owner_id", back_populates="company_owner") + class Company_types(db.Model): __tablename__ = 'company_types' @@ -48,7 +52,7 @@ class Company_legal_entities(db.Model): id = db.Column(db.Integer, nullable=False, primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(300), nullable=False) - + companies = db.relationship('Companies', backref='legal_entity', lazy='dynamic') # returns a more information-rich, or official, string representation of an object @@ -74,7 +78,7 @@ class Company_relations(db.Model): id = db.Column(db.Integer, nullable=False, primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(300), nullable=False) - + companies = db.relationship('Companies', backref='relation', lazy='dynamic') # returns a more information-rich, or official, string representation of an object diff --git a/web/minibase/blueprints/sensor/__pycache__/forms.cpython-311.pyc b/web/minibase/blueprints/sensor/__pycache__/forms.cpython-311.pyc index 76a0ad59..6dcc3c5e 100644 Binary files a/web/minibase/blueprints/sensor/__pycache__/forms.cpython-311.pyc and b/web/minibase/blueprints/sensor/__pycache__/forms.cpython-311.pyc differ diff --git a/web/minibase/blueprints/sensor/__pycache__/models.cpython-311.pyc b/web/minibase/blueprints/sensor/__pycache__/models.cpython-311.pyc index f602e8ec..fb9244c2 100644 Binary files a/web/minibase/blueprints/sensor/__pycache__/models.cpython-311.pyc and b/web/minibase/blueprints/sensor/__pycache__/models.cpython-311.pyc differ diff --git a/web/minibase/blueprints/sensor/__pycache__/routes.cpython-311.pyc b/web/minibase/blueprints/sensor/__pycache__/routes.cpython-311.pyc index ba939e3e..b239cef3 100644 Binary files a/web/minibase/blueprints/sensor/__pycache__/routes.cpython-311.pyc and b/web/minibase/blueprints/sensor/__pycache__/routes.cpython-311.pyc differ diff --git a/web/minibase/blueprints/sensor/__pycache__/utils.cpython-311.pyc b/web/minibase/blueprints/sensor/__pycache__/utils.cpython-311.pyc index 1a4f010b..a57fcf86 100644 Binary files a/web/minibase/blueprints/sensor/__pycache__/utils.cpython-311.pyc and b/web/minibase/blueprints/sensor/__pycache__/utils.cpython-311.pyc differ diff --git a/web/minibase/blueprints/sensor/forms.py b/web/minibase/blueprints/sensor/forms.py index 4e9ccca7..966b0c6f 100644 --- a/web/minibase/blueprints/sensor/forms.py +++ b/web/minibase/blueprints/sensor/forms.py @@ -23,7 +23,7 @@ class updateNbioDeviceUpdateForm(FlaskForm): # Defines the form class to be use area_id = SelectField('Area', validators=[DataRequired()]) manufacturer_id = SelectField('Manufacturer', validators=[DataRequired()]) company_id = SelectField('Managed By', validators=[DataRequired()]) - + picture = FileField('Update Sensor Picture', validators=[FileAllowed(['jpg', 'png'])]) submit = SubmitField(f'Update') def __init__(self, current_device_id, *args, **kwargs): diff --git a/web/minibase/blueprints/sensor/models.py b/web/minibase/blueprints/sensor/models.py index c8868a36..b43d65e0 100644 --- a/web/minibase/blueprints/sensor/models.py +++ b/web/minibase/blueprints/sensor/models.py @@ -19,20 +19,28 @@ class nbiotDevice(db.Model): created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) updated = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) - owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) status_id = db.Column(db.Integer, db.ForeignKey('nbiotDeviceStatus.id'), nullable=False) type_id = db.Column(db.Integer, db.ForeignKey('nbiotDeviceType.id'), nullable=False) area_id = db.Column(db.Integer, db.ForeignKey('nbiotDeviceArea.id'), nullable=False) - manufacturer_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False) - company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False) + # Foreign Keys + company_manufacturer_id = db.Column(db.Integer, db.ForeignKey('companies.id')) + company_owner_id = db.Column(db.Integer, db.ForeignKey('companies.id')) + device_user_id = db.Column(db.Integer, db.ForeignKey('users.id')) + device_owner_id = db.Column(db.Integer, db.ForeignKey('users.id')) + + # Relationships + company_manufacturer = db.relationship("Companies", foreign_keys=[company_manufacturer_id], back_populates="manufactured_devices") + company_owner = db.relationship("Companies", foreign_keys=[company_owner_id], back_populates="owned_devices") + device_user = db.relationship("Users", foreign_keys=[device_user_id], back_populates="used_devices") + device_owner = db.relationship("Users", foreign_keys=[device_owner_id], back_populates="owned_devices") class nbiotDeviceStatus(db.Model): __tablename__ = 'nbiotDeviceStatus' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(300), nullable=False) + devices = db.relationship('nbiotDevice', backref='status', lazy='dynamic') class nbiotDeviceType(db.Model): @@ -40,6 +48,7 @@ class nbiotDeviceType(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(300), nullable=False) + devices = db.relationship('nbiotDevice', backref='type', lazy='dynamic') class nbiotDeviceArea(db.Model): @@ -47,6 +56,7 @@ class nbiotDeviceArea(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) description = db.Column(db.String(300), nullable=False) + devices = db.relationship('nbiotDevice', backref='area', lazy='dynamic') class waterDetector(db.Model): diff --git a/web/minibase/blueprints/sensor/routes.py b/web/minibase/blueprints/sensor/routes.py index e0808c12..94d80aa5 100644 --- a/web/minibase/blueprints/sensor/routes.py +++ b/web/minibase/blueprints/sensor/routes.py @@ -1,4 +1,4 @@ -from flask import render_template, Blueprint, request, flash, redirect, url_for +from flask import render_template, Blueprint, request, flash, redirect, url_for, jsonify import minibase.theme as theme from minibase.blueprints.sensor.models import nbiotDevice import minibase.blueprints.sensor.utils as sensorUtils @@ -6,36 +6,52 @@ from minibase.blueprints.sensor.forms import updateNbioDeviceUpdateForm, updateN import minibase.blueprints.database.utils as dbUtils import minibase.blueprints.company.utils as companyUtils import minibase.blueprints.user.utils as userUtils +import minibase.blueprints.main.utils as mainUtils from minibase.app import db import json import os +# TODO: + sensor = Blueprint('sensor', __name__, template_folder='templates') DATA_FILE = 'data.json' -sensor.route('/callback', methods=['POST']) -def callback(): +@sensor.route('/callback', methods=['POST']) +def callback(): data = request.json if not data: - return jsonify({"error": "Invalid data"}), 400 - - # Read existing data - if os.path.exists(DATA_FILE): - with open(DATA_FILE, 'r') as f: - stored_data = json.load(f) - else: - stored_data = [] - - # Append new data - stored_data.append(data) - - # Write data back to file - with open(DATA_FILE, 'w') as f: - json.dump(stored_data, f, indent=4) - - return 'Callback received', 200 + return ({"error": "Invalid data"}), 400 + + # Access each element from the data + customer_id = data[0].get("customerId") + rcv_time = data[0].get("rcvTime") + src_imsi = data[0].get("srcImsi") + src_ip = data[0].get("srcIP") + src_port = data[0].get("srcPort") + payload = data[0].get("payload") + + # Print the elements to the console (for debugging) + #print(f"Customer ID: {customer_id}") + #print(f"Receive Time: {rcv_time}") + #print(f"Source IMSI: {src_imsi}") + #print(f"Source IP: {src_ip}") + #print(f"Source Port: {src_port}") + #print(f"Payload: {payload}") + + response_data = { + "customerId": customer_id, + "rcvTime": rcv_time, + "srcImsi": src_imsi, + "srcIP": src_ip, + "srcPort": src_port, + "payload": payload + } + sensorUtils.decode_payload(payload) + + return jsonify(response_data), 200 +#return 'data recieved', 200 @sensor.route('/data', methods=['GET']) @@ -61,14 +77,17 @@ def edit(deviceId): if deviceId: device = nbiotDevice.query.get_or_404(deviceId) form = updateNbioDeviceUpdateForm(current_device_id=device.id) - form.user_id.choices = [(row.id, row.username) for row in userUtils.queryUserNamesWithDefault(device.user_id)] - form.owner_id.choices = [(row.id, row.username) for row in userUtils.queryUserNamesWithDefault(device.user_id)] - form.manufacturer_id.choices = [(row.id, row.name) for row in companyUtils.queryNamesWithDefault(device.manufacturer_id)] - form.company_id.choices = [(row.id, row.name) for row in companyUtils.queryNamesWithDefault(device.company_id)] + form.user_id.choices = [(row.id, row.username) for row in userUtils.queryUserNamesWithDefault(device.device_user_id)] + form.owner_id.choices = [(row.id, row.username) for row in userUtils.queryUserNamesWithDefault(device.device_owner_id)] + form.manufacturer_id.choices = [(row.id, row.name) for row in companyUtils.queryNamesWithDefault(device.company_manufacturer_id)] + form.company_id.choices = [(row.id, row.name) for row in companyUtils.queryNamesWithDefault(device.company_owner_id)] form.status_id.choices = [(row.id, row.name) for row in sensorUtils.queryStatusNamesWithDefault(device.status_id)] form.type_id.choices = [(row.id, row.name) for row in sensorUtils.queryTypeNamesWithDefault(device.type_id)] form.area_id.choices = [(row.id, row.name) for row in sensorUtils.queryAreaNamesWithDefault(device.area_id)] if form.validate_on_submit(): + if form.picture.data: + picture_file = mainUtils.save_picture(form.picture.data) + device.image_file = picture_file device.name=form.name.data device.serial_no = form.serial_no.data device.device_id=form.device_id.data @@ -79,13 +98,13 @@ def edit(deviceId): device.registration_date=form.registration_date.data device.activation_date=form.activation_date.data device.deactivation_date=form.deactivation_date.data - device.owner_id=form.owner_id.data - device.user_id=form.user_id.data + device.device_owner_id=form.owner_id.data + device.device_user_id=form.user_id.data device.status_id=form.status_id.data device.type_id=form.type_id.data device.area_id=form.area_id.data - device.manufacturer_id = form.manufacturer_id.data - device.company_id = form.company_id.data + device.company_manufacturer_id = form.manufacturer_id.data + device.company_owner_id = form.company_id.data db.session.commit() flash('Device has been successfully updated', 'success') return redirect(url_for('sensor.edit', deviceId=deviceId)) @@ -100,9 +119,17 @@ def edit(deviceId): form.registration_date.data = device.registration_date form.activation_date.data = device.activation_date form.deactivation_date.data = device.deactivation_date - + form.user_id.data = device.device_user.username + form.owner_id.data = device.device_owner.username + form.manufacturer_id.data = device.company_manufacturer.name + form.company_id.data = device.company_owner.name + form.status_id.data = device.status.name + form.type_id.data = device.type.name + form.area_id.data = device.area.name + image_file = url_for('static', filename='pics/' + sensorUtils.queryImageById(deviceId)) return render_template('sensor/account.html', theme=theme, + image_file=image_file, form=form) else: flash('You need to select a Device id', 'alarm') diff --git a/web/minibase/blueprints/sensor/templates/sensor/account.html b/web/minibase/blueprints/sensor/templates/sensor/account.html index c4116c30..608c5bd1 100644 --- a/web/minibase/blueprints/sensor/templates/sensor/account.html +++ b/web/minibase/blueprints/sensor/templates/sensor/account.html @@ -5,21 +5,24 @@
- - + +
+ + + +
+
-
-
+
+
{% include 'form.html' %}
- -
-
- {{ info }} +
+
diff --git a/web/minibase/blueprints/sensor/utils.py b/web/minibase/blueprints/sensor/utils.py index 94bc54c6..89f05815 100644 --- a/web/minibase/blueprints/sensor/utils.py +++ b/web/minibase/blueprints/sensor/utils.py @@ -1,8 +1,11 @@ from minibase.blueprints.sensor.models import nbiotDevice, nbiotDeviceStatus, nbiotDeviceType, nbiotDeviceArea import minibase.blueprints.database.utils as dbUtils from sqlalchemy import case +import base64 - +def queryImageById(id): + selected = nbiotDevice.query.filter_by(id=id).first() + return selected.image_file def queryById(id): return nbiotDevice.query.filter_by(id=id).first() @@ -21,3 +24,80 @@ def queryTypeNamesWithDefault(defId): def queryAreaNamesWithDefault(defId): choices = dbUtils.queryNameWithDefaultId(nbiotDeviceArea, defId) return choices + + +def decode_payload(payload_string): + # Ensure the payload is the correct length + payload = base64.b64decode(payload_string).hex() + if len(payload) != 16: + print(f"Payload length must be 16 characters (8 bytes) current is : {len(payload)}") + raise ValueError("Payload length must be 16 characters (8 bytes)") + + # Split the payload into bytes + msb_id = payload[0:2] + second_byte_id = payload[2:4] + third_byte_id = payload[4:6] + lsb_id = payload[6:8] + battery_voltage = payload[8:10] + signal_quality = payload[10:12] + input_byte = payload[12:14] + alarm_byte = payload[14:16] + + # Convert hex strings to integers + msb_id = int(msb_id, 16) + second_byte_id = int(second_byte_id, 16) + third_byte_id = int(third_byte_id, 16) + lsb_id = int(lsb_id, 16) + battery_voltage = int(battery_voltage, 16) + signal_quality = int(signal_quality, 16) + input_byte = int(input_byte, 16) + alarm_byte = int(alarm_byte, 16) + + # Calculate voltage in mV + voltage_mv = battery_voltage * 30 + + # Determine signal quality or RSSI + if signal_quality == 99: + signal_str = "Signal quality not retrieved" + rssi = None + else: + rssi = signal_quality - 110 + signal_str = f"Signal Quality: {signal_quality} (RSSI: {rssi} dB)" + + # Decode pin status + pins_status = { + 'IN1': (input_byte & 0b00001) != 0, + 'IN2': (input_byte & 0b00010) != 0, + 'IN3': (input_byte & 0b00100) != 0, + 'IN4': (input_byte & 0b01000) != 0, + 'IN5': (input_byte & 0b10000) != 0, + } + + # Decode alarm status + alarms_status = { + 'IN1': (alarm_byte & 0b00001) != 0, + 'IN2': (alarm_byte & 0b00010) != 0, + 'IN3': (alarm_byte & 0b00100) != 0, + 'IN4': (alarm_byte & 0b01000) != 0, + 'IN5': (alarm_byte & 0b10000) != 0, + } + + # Print decoded values + uid = str(f'{msb_id:02x}{second_byte_id:02x}{third_byte_id:02x}{lsb_id:02x}') + print(f"UID: {uid}") + print(f"Battery Voltage: {battery_voltage:#04x} (Voltage: {voltage_mv/1000} V)") + for pin in range(1, 6): + pin_status = 'High' if pins_status[f'IN{pin}'] else 'Low' + alarm_status = 'Alarm' if alarms_status[f'IN{pin}'] else 'No Alarm' + print(f"Pin IN{pin} Status: {pin_status}, {alarm_status}") + print(signal_str) + + # Return decoded values in a dictionary + return { + "uid" : uid, + "battery_voltage": voltage_mv, + "signal_quality": signal_quality, + "rssi": rssi, + "pins_status": pins_status, + "alarms_status": alarms_status + } diff --git a/web/minibase/blueprints/user/__pycache__/forms.cpython-311.pyc b/web/minibase/blueprints/user/__pycache__/forms.cpython-311.pyc index 69846ceb..4c4b9cd3 100644 Binary files a/web/minibase/blueprints/user/__pycache__/forms.cpython-311.pyc and b/web/minibase/blueprints/user/__pycache__/forms.cpython-311.pyc differ diff --git a/web/minibase/blueprints/user/__pycache__/models.cpython-311.pyc b/web/minibase/blueprints/user/__pycache__/models.cpython-311.pyc index d91f7d6d..eca98c8f 100644 Binary files a/web/minibase/blueprints/user/__pycache__/models.cpython-311.pyc and b/web/minibase/blueprints/user/__pycache__/models.cpython-311.pyc differ diff --git a/web/minibase/blueprints/user/__pycache__/routes.cpython-311.pyc b/web/minibase/blueprints/user/__pycache__/routes.cpython-311.pyc index af589120..e75c6d08 100644 Binary files a/web/minibase/blueprints/user/__pycache__/routes.cpython-311.pyc and b/web/minibase/blueprints/user/__pycache__/routes.cpython-311.pyc differ diff --git a/web/minibase/blueprints/user/forms.py b/web/minibase/blueprints/user/forms.py index 6bc8bd46..f70b0e21 100644 --- a/web/minibase/blueprints/user/forms.py +++ b/web/minibase/blueprints/user/forms.py @@ -3,7 +3,7 @@ from flask_wtf.file import FileField, FileAllowed from wtforms import StringField, PasswordField, SubmitField, BooleanField, URLField from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from flask_login import current_user -from minibase.blueprints.user.models import Users +from minibase.blueprints.user.models import Users, User_Roles class registrationForm(FlaskForm): # Defines the form class to be used for the user registretion @@ -78,4 +78,3 @@ class updateRoleForm(FlaskForm): role = StringField('Role', validators=[DataRequired(), Length(min=4, max=20)]) description = StringField('Description', validators=[DataRequired(), Length(min=4, max=200)]) submit = SubmitField() - diff --git a/web/minibase/blueprints/user/models.py b/web/minibase/blueprints/user/models.py index f889104c..c19cac18 100644 --- a/web/minibase/blueprints/user/models.py +++ b/web/minibase/blueprints/user/models.py @@ -24,18 +24,19 @@ class Users(db.Model, UserMixin): last_update_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) # Relations One To Many - notes = db.relationship('Notes', backref='author', lazy='dynamic') todos = db.relationship('Todos', backref='author', lazy='dynamic') - # Here is a one to one relationship: each user can only have one role, and there can be multiple users with the same role + # Here is a one to one relationship: each user can only have one role, and there can be multiple users with the same role # type(user.role) = # type(user.role_id) = # role_id = db.Column(db.Integer, db.ForeignKey('user_roles.id'), nullable=False, default=2) role = db.relationship('User_Roles', backref=db.backref('users', lazy='dynamic')) - - + + # Relationships to Device + used_devices = db.relationship("nbiotDevice", foreign_keys="nbiotDevice.device_user_id", back_populates="device_user") + owned_devices = db.relationship("nbiotDevice", foreign_keys="nbiotDevice.device_owner_id", back_populates="device_owner") def get_reset_token(self, expires_sec=1800): s = Serializer(current_app.config['SECRET_KEY']) @@ -50,9 +51,9 @@ class Users(db.Model, UserMixin): return 0 return Users.query.get(user_id) + class User_Roles(db.Model): __tablename__ = 'user_roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20), unique=True, nullable=False) description = db.Column(db.String(200), unique=False, nullable=True) - diff --git a/web/minibase/blueprints/user/routes.py b/web/minibase/blueprints/user/routes.py index a8b94bab..5ae22103 100644 --- a/web/minibase/blueprints/user/routes.py +++ b/web/minibase/blueprints/user/routes.py @@ -5,7 +5,7 @@ import minibase.theme as theme from minibase.blueprints.user.models import Users, User_Roles import minibase.blueprints.database.utils as dbUtils import minibase.blueprints.user.utils as UserUtils -from minibase.blueprints.user.forms import registrationForm, loginForm, updateAccountForm, resetPasswordForm, requestResetForm, updateRoleForm +from minibase.blueprints.user.forms import registrationForm, loginForm, updateAccountForm, resetPasswordForm, requestResetForm, updateRoleForm import minibase.blueprints.main.utils as mainUtils # Declaring a blueprint diff --git a/web/minibase/static/pics/def_sensor.png b/web/minibase/static/pics/def_sensor.png new file mode 100644 index 00000000..a591d4ef Binary files /dev/null and b/web/minibase/static/pics/def_sensor.png differ diff --git a/web/minibase/theme.py b/web/minibase/theme.py index 1d09012b..6e122fc8 100644 --- a/web/minibase/theme.py +++ b/web/minibase/theme.py @@ -83,7 +83,7 @@ class menu: {"menuName":"Sensor", "sublinks": [ - {"text": "List", "url": "main.index"}, + {"text": "List", "url": "sensor.list"}, {"decoration": "line"}, {"text": "Add", "url": "main.index"}, {"decoration": "line"},