Compare commits
44 Commits
feature/GP
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38f23b069f | ||
|
|
1604f1d600 | ||
|
|
d2c3b4fd70 | ||
|
|
97a17b926d | ||
|
|
e30531729a | ||
|
|
c74ddc626c | ||
|
|
cedf39d95a | ||
|
|
fc9a622252 | ||
|
|
24802271bb | ||
|
|
fb53c0fb83 | ||
|
|
2e4fc9db41 | ||
|
|
1e5a8f7ee7 | ||
|
|
fc130cc92d | ||
|
|
3d9728874a | ||
|
|
7b13affb3c | ||
|
|
870933f389 | ||
|
|
934ac521a3 | ||
|
|
cdff306ca1 | ||
|
|
51862cf0a8 | ||
|
|
a50716c483 | ||
|
|
785cfc670e | ||
|
|
095dedfabb | ||
|
|
7f823cbda0 | ||
|
|
bf42eccc67 | ||
|
|
b463b866ce | ||
|
|
8dc2787b80 | ||
|
|
0ce48c18e2 | ||
|
|
02a2a2ce07 | ||
|
|
b6f13cfff8 | ||
|
|
5665cb3e5e | ||
|
|
383202709e | ||
|
|
b2c7b7677b | ||
|
|
fa1b457004 | ||
|
|
5d1b2feeab | ||
|
|
e57222f46d | ||
|
|
b8666c1727 | ||
|
|
935631a574 | ||
|
|
8c570b11b9 | ||
|
|
cf540bdec9 | ||
|
|
51db158354 | ||
|
|
0c09fcd913 | ||
|
|
6c86f85ff1 | ||
|
|
25382c3396 | ||
|
|
1b470ad910 |
373
LICENSE
Normal file
373
LICENSE
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
174
README.md
174
README.md
@@ -26,8 +26,9 @@ La encontraréis en `dist/`
|
||||
```sql
|
||||
USE dad;
|
||||
|
||||
DROP TABLE IF EXISTS co_values;
|
||||
DROP TABLE IF EXISTS weather_values;
|
||||
DROP TABLE IF EXISTS gps_values;
|
||||
DROP TABLE IF EXISTS air_values;
|
||||
DROP TABLE IF EXISTS actuators;
|
||||
DROP TABLE IF EXISTS sensors;
|
||||
DROP TABLE IF EXISTS devices;
|
||||
@@ -39,54 +40,60 @@ CREATE TABLE IF NOT EXISTS groups(
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS devices(
|
||||
deviceId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
deviceId CHAR(6) PRIMARY KEY NOT NULL,
|
||||
groupId INT NOT NULL,
|
||||
deviceName VARCHAR(64) DEFAULT NULL,
|
||||
FOREIGN KEY (groupId) REFERENCES groups(groupId)
|
||||
FOREIGN KEY (groupId) REFERENCES groups(groupId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sensors(
|
||||
sensorId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
deviceId INT NOT NULL,
|
||||
sensorId INT NOT NULL,
|
||||
deviceId CHAR(6) NOT NULL,
|
||||
sensorType VARCHAR(64) NOT NULL,
|
||||
unit VARCHAR(8) NOT NULL,
|
||||
status INT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (deviceId) REFERENCES devices(deviceId)
|
||||
PRIMARY KEY (deviceId, sensorId),
|
||||
FOREIGN KEY (deviceId) REFERENCES devices(deviceId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS actuators (
|
||||
actuatorId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
deviceId INT NOT NULL,
|
||||
actuatorId INT NOT NULL,
|
||||
deviceId CHAR(6) NOT NULL,
|
||||
status INT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (deviceId) REFERENCES devices(deviceId)
|
||||
PRIMARY KEY (deviceId, actuatorId),
|
||||
FOREIGN KEY (deviceId) REFERENCES devices(deviceId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gps_values(
|
||||
valueId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
deviceId CHAR(6) NOT NULL,
|
||||
sensorId INT NOT NULL,
|
||||
lat FLOAT NOT NULL,
|
||||
lon FLOAT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
|
||||
FOREIGN KEY (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS weather_values (
|
||||
valueId INT PRIMARY KEY AUTO_INCREMENT NOT NULL ,
|
||||
deviceId CHAR(6) NOT NULL,
|
||||
sensorId INT NOT NULL,
|
||||
temperature FLOAT NOT NULL,
|
||||
humidity FLOAT NOT NULL,
|
||||
pressure FLOAT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
|
||||
FOREIGN KEY (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS co_values (
|
||||
valueId INT PRIMARY KEY AUTO_INCREMENT NOT NULL ,
|
||||
deviceId CHAR(6) NOT NULL,
|
||||
sensorId INT NOT NULL,
|
||||
value FLOAT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
|
||||
FOREIGN KEY (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE OR REPLACE VIEW v_sensor_values AS
|
||||
@@ -98,14 +105,15 @@ SELECT
|
||||
s.status AS sensorStatus,
|
||||
wv.temperature,
|
||||
wv.humidity,
|
||||
wv.pressure,
|
||||
cv.value AS carbonMonoxide,
|
||||
gv.lat,
|
||||
gv.lon,
|
||||
COALESCE(gv.timestamp, wv.timestamp, cv.timestamp) AS timestamp -- el primero no nulo
|
||||
FROM sensors s
|
||||
LEFT JOIN weather_values wv ON s.sensorId = wv.sensorId
|
||||
LEFT JOIN co_values cv ON s.sensorId = cv.sensorId
|
||||
LEFT JOIN gps_values gv ON s.sensorId = gv.sensorId;
|
||||
LEFT JOIN weather_values wv ON s.deviceId = wv.deviceId AND s.sensorId = wv.sensorId
|
||||
LEFT JOIN co_values cv ON s.deviceId = cv.deviceId AND s.sensorId = cv.sensorId
|
||||
LEFT JOIN gps_values gv ON s.deviceId = gv.deviceId AND s.sensorId = gv.sensorId;
|
||||
|
||||
|
||||
CREATE OR REPLACE VIEW v_latest_values AS
|
||||
@@ -118,14 +126,15 @@ SELECT
|
||||
s.timestamp AS sensorTimestamp,
|
||||
wv.temperature,
|
||||
wv.humidity,
|
||||
wv.pressure,
|
||||
cv.value AS carbonMonoxide,
|
||||
gv.lat,
|
||||
gv.lon,
|
||||
COALESCE(gv.timestamp, wv.timestamp, cv.timestamp) AS airValuesTimestamp -- el primero no nulo
|
||||
FROM sensors s
|
||||
LEFT JOIN weather_values wv ON s.sensorId = wv.sensorId
|
||||
LEFT JOIN co_values cv ON s.sensorId = cv.sensorId
|
||||
LEFT JOIN gps_values gv ON s.sensorId = gv.sensorId
|
||||
LEFT JOIN weather_values wv ON s.deviceId = wv.deviceId AND s.sensorId = wv.sensorId
|
||||
LEFT JOIN co_values cv ON s.deviceId = cv.deviceId AND s.sensorId = cv.sensorId
|
||||
LEFT JOIN gps_values gv ON s.deviceId = gv.deviceId AND s.sensorId = gv.sensorId
|
||||
WHERE (wv.timestamp = (SELECT MAX(timestamp) FROM weather_values WHERE sensorId = s.sensorId)
|
||||
OR cv.timestamp = (SELECT MAX(timestamp) FROM co_values WHERE sensorId = s.sensorId)
|
||||
OR gv.timestamp = (SELECT MAX(timestamp) FROM gps_values WHERE sensorId = s.sensorId));
|
||||
@@ -138,7 +147,7 @@ SELECT
|
||||
c.value AS carbonMonoxide,
|
||||
c.timestamp
|
||||
FROM sensors s
|
||||
JOIN co_values c ON s.sensorId = c.sensorId
|
||||
JOIN co_values c ON s.deviceId = c.deviceId AND s.sensorId = c.sensorId
|
||||
WHERE s.sensorType = 'CO';
|
||||
|
||||
CREATE OR REPLACE VIEW v_gps_by_device AS
|
||||
@@ -148,7 +157,7 @@ SELECT
|
||||
g.lon,
|
||||
g.timestamp
|
||||
FROM sensors s
|
||||
JOIN gps_values g ON s.sensorId = g.sensorId
|
||||
JOIN gps_values g ON s.deviceId = g.deviceId AND s.sensorId = g.sensorId
|
||||
WHERE s.sensorType = 'GPS';
|
||||
|
||||
CREATE OR REPLACE VIEW v_weather_by_device AS
|
||||
@@ -156,100 +165,89 @@ SELECT
|
||||
s.deviceId,
|
||||
w.temperature AS temperature,
|
||||
w.humidity AS humidity,
|
||||
w.pressure AS pressure,
|
||||
w.timestamp
|
||||
FROM sensors s
|
||||
JOIN weather_values w ON s.sensorId = w.sensorId
|
||||
JOIN weather_values w ON s.deviceId = w.deviceId AND s.sensorId = w.sensorId
|
||||
WHERE s.sensorType = 'Temperature & Humidity';
|
||||
-- VISTAS AUXILIARES
|
||||
|
||||
CREATE OR REPLACE VIEW v_pollution_map AS
|
||||
SELECT
|
||||
d.deviceId AS deviceId,
|
||||
d.deviceName AS deviceName,
|
||||
g.lat AS lat,
|
||||
g.lon AS lon,
|
||||
c.carbonMonoxide AS carbonMonoxide,
|
||||
c.timestamp AS timestamp
|
||||
FROM
|
||||
(dad.devices d
|
||||
LEFT JOIN dad.v_co_by_device c ON d.deviceId = c.deviceId)
|
||||
LEFT JOIN dad.v_gps_by_device g ON d.deviceId = g.deviceId AND (g.timestamp <= c.timestamp OR g.timestamp IS NULL)
|
||||
WHERE
|
||||
c.carbonMonoxide IS NOT NULL
|
||||
ORDER BY
|
||||
d.deviceId,
|
||||
d.deviceName,
|
||||
g.lat,
|
||||
g.lon,
|
||||
c.carbonMonoxide,
|
||||
c.timestamp
|
||||
FROM devices d
|
||||
LEFT JOIN v_co_by_device c ON d.deviceId = c.deviceId
|
||||
LEFT JOIN v_gps_by_device g ON d.deviceId = g.deviceId
|
||||
AND (g.timestamp <= c.timestamp OR g.timestamp IS NULL)
|
||||
WHERE c.carbonMonoxide IS NOT NULL
|
||||
ORDER BY d.deviceId, c.timestamp;
|
||||
c.timestamp;
|
||||
|
||||
CREATE OR REPLACE VIEW v_sensor_history_by_device AS
|
||||
SELECT
|
||||
d.deviceId,
|
||||
d.deviceName,
|
||||
d.deviceId AS deviceId,
|
||||
d.deviceName AS deviceName,
|
||||
w.temperature AS value,
|
||||
'temperature' AS valueType,
|
||||
w.timestamp
|
||||
FROM devices d
|
||||
JOIN v_weather_by_device w ON d.deviceId = w.deviceId
|
||||
w.timestamp AS timestamp
|
||||
FROM
|
||||
dad.devices d
|
||||
JOIN dad.v_weather_by_device w ON d.deviceId = w.deviceId
|
||||
UNION ALL
|
||||
SELECT
|
||||
d.deviceId,
|
||||
d.deviceName,
|
||||
d.deviceId AS deviceId,
|
||||
d.deviceName AS deviceName,
|
||||
w.humidity AS value,
|
||||
'humidity' AS valueType,
|
||||
w.timestamp
|
||||
FROM devices d
|
||||
JOIN v_weather_by_device w ON d.deviceId = w.deviceId
|
||||
w.timestamp AS timestamp
|
||||
FROM
|
||||
dad.devices d
|
||||
JOIN dad.v_weather_by_device w ON d.deviceId = w.deviceId
|
||||
UNION ALL
|
||||
SELECT
|
||||
d.deviceId,
|
||||
d.deviceName,
|
||||
d.deviceId AS deviceId,
|
||||
d.deviceName AS deviceName,
|
||||
w.pressure AS value,
|
||||
'pressure' AS valueType,
|
||||
w.timestamp AS timestamp
|
||||
FROM
|
||||
dad.devices d
|
||||
JOIN dad.v_weather_by_device w ON d.deviceId = w.deviceId
|
||||
UNION ALL
|
||||
SELECT
|
||||
d.deviceId AS deviceId,
|
||||
d.deviceName AS deviceName,
|
||||
c.carbonMonoxide AS value,
|
||||
'carbonMonoxide' AS valueType,
|
||||
c.timestamp
|
||||
FROM devices d
|
||||
JOIN v_co_by_device c ON d.deviceId = c.deviceId
|
||||
ORDER BY deviceId, timestamp;
|
||||
c.timestamp AS timestamp
|
||||
FROM
|
||||
dad.devices d
|
||||
JOIN dad.v_co_by_device c ON d.deviceId = c.deviceId
|
||||
ORDER BY
|
||||
deviceId,
|
||||
timestamp;
|
||||
|
||||
-- Insertar grupos
|
||||
INSERT INTO groups (groupName) VALUES ('Grupo 1');
|
||||
|
||||
-- Insertar dispositivos
|
||||
INSERT INTO devices (groupId, deviceName) VALUES
|
||||
(1, 'Dispositivo 1'),
|
||||
(1, 'Dispositivo 2'),
|
||||
(1, 'Dispositivo 3');
|
||||
INSERT INTO devices (deviceId, groupId, deviceName) VALUES
|
||||
('6A6098', 1, 'Dispositivo 1');
|
||||
|
||||
-- Sensores para el Dispositivo 1
|
||||
-- Cada dispositivo tiene un único sensor de cada tipo (GPS, Temperature & Humidity, CO)
|
||||
INSERT INTO sensors (deviceId, sensorType, unit, status) VALUES
|
||||
(1, 'GPS', 'N/A', 1), -- Sensor de GPS para Dispositivo 1
|
||||
(1, 'Temperature & Humidity', '°C/%', 1), -- Sensor de Temp/Humidity para Dispositivo 1
|
||||
(1, 'CO', 'ppm', 1); -- Sensor de CO para Dispositivo 1
|
||||
-- Sensores para el Dispositivo 6A6098
|
||||
INSERT INTO sensors (sensorId, deviceId, sensorType, unit, status) VALUES
|
||||
(1, '6A6098', 'GPS', 'N/A', 1),
|
||||
(2, '6A6098', 'Temperature & Humidity', '°C/%', 1),
|
||||
(3, '6A6098', 'CO', 'ppm', 1);
|
||||
|
||||
-- Sensores para el Dispositivo 2
|
||||
INSERT INTO sensors (deviceId, sensorType, unit, status) VALUES
|
||||
(2, 'GPS', 'N/A', 1), -- Sensor de GPS para Dispositivo 2
|
||||
(2, 'Temperature & Humidity', '°C/%', 1), -- Sensor de Temp/Humidity para Dispositivo 2
|
||||
(2, 'CO', 'ppm', 1); -- Sensor de CO para Dispositivo 2
|
||||
|
||||
-- Sensores para el Dispositivo 3
|
||||
INSERT INTO sensors (deviceId, sensorType, unit, status) VALUES
|
||||
(3, 'GPS', 'N/A', 1), -- Sensor de GPS para Dispositivo 3
|
||||
(3, 'Temperature & Humidity', '°C/%', 1), -- Sensor de Temp/Humidity para Dispositivo 3
|
||||
(3, 'CO', 'ppm', 1); -- Sensor de CO para Dispositivo 3
|
||||
|
||||
-- Valores de GPS para los Dispositivos
|
||||
-- Cada dispositivo tiene un único sensor de GPS con latitud y longitud asociada
|
||||
INSERT INTO gps_values (sensorId, lat, lon) VALUES
|
||||
(1, 37.3861, -5.9921), -- GPS para Dispositivo 1
|
||||
(4, 37.3850, -5.9910), -- GPS para Dispositivo 2
|
||||
(7, 37.3860, -5.9920); -- GPS para Dispositivo 3
|
||||
|
||||
-- Valores de Temperatura, Humedad y CO para los Dispositivos
|
||||
-- Cada dispositivo tiene un único sensor de aire (temperatura, humedad, CO) con valores asociados
|
||||
INSERT INTO weather_values (sensorId, temperature, humidity) VALUES
|
||||
(2, 22.5, 45.0), -- Temperatura, Humedad para Dispositivo 1
|
||||
(5, 24.5, 50.0), -- Temperatura, Humedad para Dispositivo 2
|
||||
(8, 21.0, 44.0); -- Temperatura, Humedad para Dispositivo 3
|
||||
|
||||
INSERT INTO co_values (sensorId, value) VALUES
|
||||
(3, 0.02), -- CO para Dispositivo 1
|
||||
(6, 0.04), -- CO para Dispositivo 2
|
||||
(9, 0.01); -- CO para Dispositivo 3
|
||||
-- ACtuadores para el Dispositivo 6A6098
|
||||
INSERT INTO actuators (actuatorId, deviceId, status, timestamp) VALUES
|
||||
(1, '6A6098', 1, CURRENT_TIMESTAMP());
|
||||
```
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.X MariaDB/MySQL Client -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mysql-client</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.X Web -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
@@ -46,13 +53,6 @@
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JDBC Driver -->
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Gson -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
@@ -60,11 +60,44 @@
|
||||
<version>2.12.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/io.quarkus/quarkus-agroal -->
|
||||
<!-- SLF4J + Logback -->
|
||||
<dependency>
|
||||
<groupId>org.jboss.logmanager</groupId>
|
||||
<artifactId>jboss-logmanager</artifactId>
|
||||
<version>3.1.1.Final</version> <!-- O la versión más reciente -->
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.12</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.5.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.locationtech.jts/jts-core -->
|
||||
<dependency>
|
||||
<groupId>org.locationtech.jts</groupId>
|
||||
<artifactId>jts-core</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.locationtech.jts.io/jts-io-common -->
|
||||
<dependency>
|
||||
<groupId>org.locationtech.jts.io</groupId>
|
||||
<artifactId>jts-io-common</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>2.19.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web-api-contract</artifactId>
|
||||
<version>4.5.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package net.miarma.contaminus.common;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.sqlclient.Row;
|
||||
|
||||
public abstract class AbstractEntity {
|
||||
|
||||
public AbstractEntity() {}
|
||||
|
||||
public AbstractEntity(Row row) {
|
||||
populateFromRow(row);
|
||||
}
|
||||
|
||||
private void populateFromRow(Row row) {
|
||||
Field[] fields = this.getClass().getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Class<?> type = field.getType();
|
||||
String name = field.getName();
|
||||
|
||||
Object value;
|
||||
if (type.isEnum()) {
|
||||
Integer intValue = row.getInteger(name);
|
||||
if (intValue != null) {
|
||||
try {
|
||||
var method = type.getMethod("fromInt", int.class);
|
||||
value = method.invoke(null, intValue);
|
||||
} catch (Exception e) {
|
||||
value = null;
|
||||
}
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
} else {
|
||||
value = switch (type.getSimpleName()) {
|
||||
case "Integer" -> row.getInteger(name);
|
||||
case "String" -> row.getString(name);
|
||||
case "Double" -> row.getDouble(name);
|
||||
case "Long" -> row.getLong(name);
|
||||
case "Boolean" -> row.getBoolean(name);
|
||||
case "int" -> row.getInteger(name);
|
||||
case "double" -> row.getDouble(name);
|
||||
case "long" -> row.getLong(name);
|
||||
case "boolean" -> row.getBoolean(name);
|
||||
case "LocalDateTime" -> row.getLocalDateTime(name);
|
||||
case "BigDecimal" -> {
|
||||
try {
|
||||
var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(name));
|
||||
yield numeric != null ? numeric.bigDecimalValue() : null;
|
||||
} catch (Exception e) {
|
||||
yield null;
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
System.err.println("Type not supported yet: " + type.getName() + " for field " + name);
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
field.set(this, value);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String encode() {
|
||||
JsonObject json = new JsonObject();
|
||||
Class<?> clazz = this.getClass();
|
||||
|
||||
while (clazz != null) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object value = field.get(this);
|
||||
|
||||
if (value instanceof ValuableEnum ve) {
|
||||
json.put(field.getName(), ve.getValue());
|
||||
} else {
|
||||
json.put(field.getName(), value);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
|
||||
return json.encode();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(this.getClass().getSimpleName()).append(" [ ");
|
||||
Field[] fields = this.getClass().getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
sb.append(field.getName()).append("= ").append(field.get(this)).append(", ");
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +1,46 @@
|
||||
package net.miarma.contaminus.common;
|
||||
|
||||
import io.vertx.core.impl.logging.Logger;
|
||||
import io.vertx.core.impl.logging.LoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Constants {
|
||||
public static final String APP_NAME = "ContaminUS";
|
||||
public static final String API_PREFIX = "/api/v1";
|
||||
public static final String RAW_API_PREFIX = "/api/raw/v1";
|
||||
public static final String CONTAMINUS_EB = "contaminus.eventbus";
|
||||
public static Logger LOGGER = LoggerFactory.getLogger(Constants.APP_NAME);
|
||||
|
||||
public static final Integer SENSOR_ROLE = 0;
|
||||
public static final Integer ACTUATOR_ROLE = 1;
|
||||
|
||||
/* API Endpoints */
|
||||
public static final String GET_GROUPS = RAW_API_PREFIX + "/groups";
|
||||
public static final String POST_GROUPS = RAW_API_PREFIX + "/groups";
|
||||
public static final String PUT_GROUP_BY_ID = RAW_API_PREFIX + "/groups/:groupId";
|
||||
public static final String GROUPS = RAW_API_PREFIX + "/groups"; // GET, POST
|
||||
public static final String GROUP = RAW_API_PREFIX + "/groups/:groupId"; // GET, PUT
|
||||
|
||||
public static final String GET_DEVICES = RAW_API_PREFIX + "/devices";
|
||||
public static final String POST_DEVICES = RAW_API_PREFIX + "/devices";
|
||||
public static final String PUT_DEVICE_BY_ID = RAW_API_PREFIX + "/devices/:deviceId";
|
||||
public static final String DEVICES = RAW_API_PREFIX + "/groups/:groupId/devices"; // GET, POST
|
||||
public static final String DEVICE = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId"; // GET, PUT
|
||||
public static final String LATEST_VALUES = API_PREFIX + "/groups/:groupId/devices/:deviceId/latest-values"; // GET
|
||||
public static final String POLLUTION_MAP = API_PREFIX + "/groups/:groupId/devices/:deviceId/pollution-map"; // GET
|
||||
public static final String HISTORY = API_PREFIX + "/groups/:groupId/devices/:deviceId/history"; // GET
|
||||
public static final String DEVICE_GROUP_ID = RAW_API_PREFIX + "/devices/:deviceId/my-group"; // GET
|
||||
|
||||
public static final String GET_SENSORS = RAW_API_PREFIX + "/sensors";
|
||||
public static final String POST_SENSORS = RAW_API_PREFIX + "/sensors";
|
||||
public static final String PUT_SENSOR_BY_ID = RAW_API_PREFIX + "/sensors/:sensorId";
|
||||
public static final String SENSORS = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors"; // GET, POST
|
||||
public static final String SENSOR = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId"; // GET, PUT
|
||||
public static final String SENSOR_VALUES = API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId/values"; // GET
|
||||
|
||||
public static final String GET_ACTUATORS = RAW_API_PREFIX + "/actuators";
|
||||
public static final String POST_ACTUATORS = RAW_API_PREFIX + "/actuators";
|
||||
public static final String PUT_ACTUATOR_BY_ID = RAW_API_PREFIX + "/actuators/:actuatorId";
|
||||
public static final String BATCH = API_PREFIX + "/batch"; // POST
|
||||
public static final String ADD_GPS_VALUE = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId/gps_values"; // POST
|
||||
public static final String ADD_WEATHER_VALUE = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId/weather_values"; // POST
|
||||
public static final String ADD_CO_VALUE = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId/co_values"; // POST
|
||||
|
||||
public static final String GET_CO_BY_DEVICE_VIEW = RAW_API_PREFIX + "/v_co_by_device";
|
||||
public static final String ACTUATORS = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators"; // GET, POST
|
||||
public static final String ACTUATOR = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuatorId"; // GET, PUT
|
||||
public static final String ACTUATOR_STATUS = API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status"; // GET, PUT
|
||||
|
||||
public static final String GET_GPS_BY_DEVICE_VIEW = RAW_API_PREFIX + "/v_gps_by_device";
|
||||
public static final String GET_LATEST_VALUES_VIEW = RAW_API_PREFIX + "/v_latest_values";
|
||||
public static final String GET_POLLUTION_MAP_VIEW = RAW_API_PREFIX + "/v_pollution_map";
|
||||
public static final String GET_SENSOR_HISTORY_BY_DEVICE_VIEW = RAW_API_PREFIX + "/v_sensor_history_by_device";
|
||||
public static final String GET_SENSOR_VALUES_VIEW = RAW_API_PREFIX + "/v_sensor_values";
|
||||
public static final String GET_WEATHER_BY_DEVICE_VIEW = RAW_API_PREFIX + "/v_weather_by_device";
|
||||
|
||||
/* Bussiness Logic API */
|
||||
public static final String GET_GROUP_BY_ID = API_PREFIX + "/groups/:groupId";
|
||||
public static final String GET_GROUP_DEVICES = API_PREFIX + "/groups/:groupId/devices";
|
||||
public static final String GET_DEVICE_BY_ID = API_PREFIX + "/devices/:deviceId";
|
||||
public static final String GET_DEVICE_SENSORS = API_PREFIX + "/devices/:deviceId/sensors";
|
||||
public static final String GET_DEVICE_ACTUATORS = API_PREFIX + "/devices/:deviceId/actuators";
|
||||
public static final String GET_DEVICE_LATEST_VALUES = API_PREFIX + "/devices/:deviceId/latest";
|
||||
public static final String GET_DEVICE_POLLUTION_MAP = API_PREFIX + "/devices/:deviceId/pollution-map";
|
||||
public static final String GET_DEVICE_HISTORY = API_PREFIX + "/devices/:deviceId/history";
|
||||
public static final String GET_SENSOR_BY_ID = API_PREFIX + "/sensors/:sensorId";
|
||||
public static final String GET_SENSOR_VALUES = API_PREFIX + "/sensors/:sensorId/values";
|
||||
public static final String GET_ACTUATOR_BY_ID = API_PREFIX + "/actuators/:actuatorId";
|
||||
public static final String VIEW_LATEST_VALUES = RAW_API_PREFIX + "/v_latest_values"; // GET
|
||||
public static final String VIEW_POLLUTION_MAP = RAW_API_PREFIX + "/v_pollution_map"; // GET
|
||||
public static final String VIEW_SENSOR_HISTORY = RAW_API_PREFIX + "/v_sensor_history_by_device"; // GET
|
||||
public static final String VIEW_SENSOR_VALUES = RAW_API_PREFIX + "/v_sensor_values"; // GET
|
||||
|
||||
private Constants() {
|
||||
throw new AssertionError("Utility class cannot be instantiated.");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.miarma.contaminus.common;
|
||||
|
||||
public interface ValuableEnum {
|
||||
int getValue();
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package net.miarma.contaminus.common;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class VoronoiZoneDetector {
|
||||
|
||||
private static class Zone {
|
||||
Polygon polygon;
|
||||
Integer groupId;
|
||||
|
||||
public Zone(Polygon polygon, Integer groupId) {
|
||||
this.polygon = polygon;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<Zone> zones = new ArrayList<>();
|
||||
private static final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private VoronoiZoneDetector(String geojsonUrl, boolean isUrl) {
|
||||
String geojsonStr;
|
||||
|
||||
try {
|
||||
if(isUrl) {
|
||||
try(InputStream is = URL.of(URI.create(geojsonUrl), null).openStream()) {
|
||||
geojsonStr = new String(is.readAllBytes());
|
||||
}
|
||||
} else {
|
||||
geojsonStr = Files.readString(new File(geojsonUrl).toPath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Constants.LOGGER.error("⚠️ Error al cargar el GeoJSON: " + e.getMessage());
|
||||
throw new RuntimeException("Error al cargar el GeoJSON", e);
|
||||
}
|
||||
|
||||
JsonObject root = JsonParser.parseString(geojsonStr).getAsJsonObject();
|
||||
JsonArray features = root.getAsJsonArray("features");
|
||||
GeoJsonReader reader = new GeoJsonReader(geometryFactory);
|
||||
|
||||
for (int i = 0; i < features.size(); i++) {
|
||||
JsonObject feature = features.get(i).getAsJsonObject();
|
||||
|
||||
Integer groupId = feature
|
||||
.getAsJsonObject("properties")
|
||||
.get("groupId")
|
||||
.getAsInt();
|
||||
|
||||
JsonObject geometryJson = feature.getAsJsonObject("geometry");
|
||||
String geometryStr = gson.toJson(geometryJson);
|
||||
|
||||
Geometry geometry = null;
|
||||
try {
|
||||
geometry = reader.read(geometryStr);
|
||||
} catch (ParseException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (geometry instanceof Polygon polygon) {
|
||||
zones.add(new Zone(polygon, groupId));
|
||||
} else {
|
||||
Constants.LOGGER.error("⚠️ Geometría ignorada: no es un polígono");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static VoronoiZoneDetector create(String geojsonUrl, boolean isUrl) {
|
||||
return new VoronoiZoneDetector(geojsonUrl, isUrl);
|
||||
}
|
||||
|
||||
public Integer getZoneForPoint(double lon, double lat) {
|
||||
Point p = geometryFactory.createPoint(new Coordinate(lon, lat));
|
||||
|
||||
for (Zone z : zones) {
|
||||
if (z.polygon.covers(p)) {
|
||||
return z.groupId;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // no está dentro de ninguna zona
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
VoronoiZoneDetector detector = new VoronoiZoneDetector("https://miarma.net/files/voronoi_sevilla_geovoronoi.geojson", true);
|
||||
|
||||
double lon = -5.9752;
|
||||
double lat = 37.3887;
|
||||
|
||||
Integer actuatorId = detector.getZoneForPoint(lon, lat);
|
||||
if (actuatorId != null) {
|
||||
System.out.println("📍 El punto pertenece al actuator: " + actuatorId);
|
||||
} else {
|
||||
System.out.println("🚫 El punto no pertenece a ninguna zona");
|
||||
}
|
||||
}
|
||||
}
|
||||
130
backend/src/main/java/net/miarma/contaminus/dao/ActuatorDAO.java
Normal file
130
backend/src/main/java/net/miarma/contaminus/dao/ActuatorDAO.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.Actuator;
|
||||
|
||||
public class ActuatorDAO implements DataAccessObject<Actuator, Integer>{
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public ActuatorDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<Actuator>> getAll() {
|
||||
Promise<List<Actuator>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(Actuator.class).build();
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<List<Actuator>> getAllByDeviceId(String deviceId) {
|
||||
Promise<List<Actuator>> promise = Promise.promise();
|
||||
Actuator actuator = new Actuator();
|
||||
actuator.setDeviceId(deviceId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Actuator.class)
|
||||
.where(actuator)
|
||||
.build();
|
||||
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Actuator> getById(Integer id) {
|
||||
Promise<Actuator> promise = Promise.promise();
|
||||
Actuator actuator = new Actuator();
|
||||
actuator.setActuatorId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Actuator.class)
|
||||
.where(actuator)
|
||||
.build();
|
||||
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<Actuator> getByIdAndDeviceId(Integer actuatorId, String deviceId) {
|
||||
Promise<Actuator> promise = Promise.promise();
|
||||
Actuator actuator = new Actuator();
|
||||
actuator.setDeviceId(deviceId);
|
||||
actuator.setActuatorId(actuatorId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Actuator.class)
|
||||
.where(actuator)
|
||||
.build();
|
||||
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Actuator> insert(Actuator t) {
|
||||
Promise<Actuator> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Actuator> update(Actuator t) {
|
||||
Promise<Actuator> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
System.out.println();
|
||||
|
||||
db.execute(query, Actuator.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Actuator> delete(Integer id) {
|
||||
Promise<Actuator> promise = Promise.promise();
|
||||
Actuator actuator = new Actuator();
|
||||
actuator.setActuatorId(id);
|
||||
|
||||
String query = QueryBuilder.delete(actuator).build();
|
||||
|
||||
db.executeOne(query, Actuator.class,
|
||||
_ -> promise.complete(actuator),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.COValue;
|
||||
|
||||
public class COValueDAO implements DataAccessObject<COValue, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public COValueDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<COValue>> getAll() {
|
||||
Promise<List<COValue>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(COValue.class).build();
|
||||
db.execute(query, COValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<COValue> getById(Integer id) {
|
||||
Promise<COValue> promise = Promise.promise();
|
||||
COValue coValue = new COValue();
|
||||
coValue.setValueId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(COValue.class)
|
||||
.where(coValue)
|
||||
.build();
|
||||
|
||||
db.execute(query, COValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<COValue> insert(COValue t) {
|
||||
Promise<COValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, COValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<COValue> update(COValue t) {
|
||||
Promise<COValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, COValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<COValue> delete(Integer id) {
|
||||
throw new UnsupportedOperationException("Cannot delete samples");
|
||||
}
|
||||
|
||||
}
|
||||
128
backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java
Normal file
128
backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java
Normal file
@@ -0,0 +1,128 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.Device;
|
||||
|
||||
public class DeviceDAO implements DataAccessObject<Device, String> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public DeviceDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<Device>> getAll() {
|
||||
Promise<List<Device>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(Device.class).build();
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<List<Device>> getAllByGroupId(Integer groupId) {
|
||||
Promise<List<Device>> promise = Promise.promise();
|
||||
Device device = new Device();
|
||||
device.setGroupId(groupId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Device.class)
|
||||
.where(device)
|
||||
.build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Device> getById(String id) {
|
||||
Promise<Device> promise = Promise.promise();
|
||||
Device device = new Device();
|
||||
device.setDeviceId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Device.class)
|
||||
.where(device)
|
||||
.build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<Device> getByIdAndGroupId(String id, Integer groupId) {
|
||||
Promise<Device> promise = Promise.promise();
|
||||
Device device = new Device();
|
||||
device.setDeviceId(id);
|
||||
device.setGroupId(groupId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Device.class)
|
||||
.where(device)
|
||||
.build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Device> insert(Device t) {
|
||||
Promise<Device> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Device> update(Device t) {
|
||||
Promise<Device> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Device> delete(String id) {
|
||||
Promise<Device> promise = Promise.promise();
|
||||
Device device = new Device();
|
||||
device.setDeviceId(id);
|
||||
String query = QueryBuilder.delete(device).build();
|
||||
|
||||
db.execute(query, Device.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.GpsValue;
|
||||
|
||||
public class GpsValueDAO implements DataAccessObject<GpsValue, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public GpsValueDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<GpsValue>> getAll() {
|
||||
Promise<List<GpsValue>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(GpsValue.class).build();
|
||||
db.execute(query, GpsValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<GpsValue> getById(Integer id) {
|
||||
Promise<GpsValue> promise = Promise.promise();
|
||||
GpsValue gpsValue = new GpsValue();
|
||||
gpsValue.setValueId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(GpsValue.class)
|
||||
.where(gpsValue)
|
||||
.build();
|
||||
|
||||
db.execute(query, GpsValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<GpsValue> insert(GpsValue t) {
|
||||
Promise<GpsValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, GpsValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<GpsValue> update(GpsValue t) {
|
||||
Promise<GpsValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, GpsValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<GpsValue> delete(Integer id) {
|
||||
throw new UnsupportedOperationException("Cannot delete samples");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.Group;
|
||||
|
||||
public class GroupDAO implements DataAccessObject<Group, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public GroupDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<Group>> getAll() {
|
||||
Promise<List<Group>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(Group.class).build();
|
||||
db.execute(query, Group.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Group> getById(Integer id) {
|
||||
Promise<Group> promise = Promise.promise();
|
||||
Group group = new Group();
|
||||
group.setGroupId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Group.class)
|
||||
.where(group)
|
||||
.build();
|
||||
|
||||
db.execute(query, Group.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Group> insert(Group t) {
|
||||
Promise<Group> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, Group.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Group> update(Group t) {
|
||||
Promise<Group> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, Group.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Group> delete(Integer id) {
|
||||
Promise<Group> promise = Promise.promise();
|
||||
Group group = new Group();
|
||||
group.setGroupId(id);
|
||||
|
||||
String query = QueryBuilder.delete(group).build();
|
||||
|
||||
db.execute(query, Group.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
}
|
||||
130
backend/src/main/java/net/miarma/contaminus/dao/SensorDAO.java
Normal file
130
backend/src/main/java/net/miarma/contaminus/dao/SensorDAO.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.Sensor;
|
||||
|
||||
public class SensorDAO implements DataAccessObject<Sensor, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public SensorDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<Sensor>> getAll() {
|
||||
Promise<List<Sensor>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(Sensor.class).build();
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<List<Sensor>> getAllByDeviceId(String deviceId) {
|
||||
Promise<List<Sensor>> promise = Promise.promise();
|
||||
Sensor sensor = new Sensor();
|
||||
sensor.setDeviceId(deviceId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Sensor.class)
|
||||
.where(sensor)
|
||||
.build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Sensor> getById(Integer id) {
|
||||
Promise<Sensor> promise = Promise.promise();
|
||||
Sensor sensor = new Sensor();
|
||||
sensor.setSensorId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Sensor.class)
|
||||
.where(sensor)
|
||||
.build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<Sensor> getByIdAndDeviceId(Integer sensorId, String deviceId) {
|
||||
Promise<Sensor> promise = Promise.promise();
|
||||
Sensor sensor = new Sensor();
|
||||
sensor.setDeviceId(deviceId);
|
||||
sensor.setSensorId(sensorId);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(Sensor.class)
|
||||
.where(sensor)
|
||||
.build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Sensor> insert(Sensor t) {
|
||||
Promise<Sensor> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Sensor> update(Sensor t) {
|
||||
Promise<Sensor> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Sensor> delete(Integer id) {
|
||||
Promise<Sensor> promise = Promise.promise();
|
||||
Sensor sensor = new Sensor();
|
||||
|
||||
sensor.setSensorId(id);
|
||||
|
||||
String query = QueryBuilder.delete(sensor).build();
|
||||
|
||||
db.execute(query, Sensor.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.miarma.contaminus.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.WeatherValue;
|
||||
|
||||
public class WeatherValueDAO implements DataAccessObject<WeatherValue, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public WeatherValueDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<WeatherValue>> getAll() {
|
||||
Promise<List<WeatherValue>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(WeatherValue.class).build();
|
||||
db.execute(query, WeatherValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<WeatherValue> getById(Integer id) {
|
||||
Promise<WeatherValue> promise = Promise.promise();
|
||||
WeatherValue weatherValue = new WeatherValue();
|
||||
weatherValue.setValueId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(WeatherValue.class)
|
||||
.where(weatherValue)
|
||||
.build();
|
||||
|
||||
db.execute(query, WeatherValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<WeatherValue> insert(WeatherValue t) {
|
||||
Promise<WeatherValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.insert(t).build();
|
||||
|
||||
db.execute(query, WeatherValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<WeatherValue> update(WeatherValue t) {
|
||||
Promise<WeatherValue> promise = Promise.promise();
|
||||
String query = QueryBuilder.update(t).build();
|
||||
|
||||
db.execute(query, WeatherValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<WeatherValue> delete(Integer id) {
|
||||
throw new UnsupportedOperationException("Cannot delete samples");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.miarma.contaminus.dao.views;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.ViewLatestValues;
|
||||
|
||||
public class ViewLatestValuesDAO implements DataAccessObject<ViewLatestValues, String> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public ViewLatestValuesDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<ViewLatestValues>> getAll() {
|
||||
Promise<List<ViewLatestValues>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(ViewLatestValues.class).build();
|
||||
db.execute(query, ViewLatestValues.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewLatestValues> getById(String id) {
|
||||
Promise<ViewLatestValues> promise = Promise.promise();
|
||||
ViewLatestValues view = new ViewLatestValues();
|
||||
view.setDeviceId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(ViewLatestValues.class)
|
||||
.where(view)
|
||||
.build();
|
||||
|
||||
db.execute(query, ViewLatestValues.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewLatestValues> insert(ViewLatestValues t) {
|
||||
throw new UnsupportedOperationException("Insert not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewLatestValues> update(ViewLatestValues t) {
|
||||
throw new UnsupportedOperationException("Update not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewLatestValues> delete(String id) {
|
||||
throw new UnsupportedOperationException("Delete not supported for views");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.miarma.contaminus.dao.views;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.ViewPollutionMap;
|
||||
|
||||
@Table("v_pollution_map")
|
||||
public class ViewPollutionMapDAO implements DataAccessObject<ViewPollutionMap, String> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
public ViewPollutionMapDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<ViewPollutionMap>> getAll() {
|
||||
Promise<List<ViewPollutionMap>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(ViewPollutionMap.class).build();
|
||||
db.execute(query, ViewPollutionMap.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewPollutionMap> getById(String id) {
|
||||
Promise<ViewPollutionMap> promise = Promise.promise();
|
||||
ViewPollutionMap view = new ViewPollutionMap();
|
||||
view.setDeviceId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(ViewPollutionMap.class)
|
||||
.where(view)
|
||||
.build();
|
||||
|
||||
db.execute(query, ViewPollutionMap.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewPollutionMap> insert(ViewPollutionMap t) {
|
||||
throw new UnsupportedOperationException("Insert not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewPollutionMap> update(ViewPollutionMap t) {
|
||||
throw new UnsupportedOperationException("Update not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewPollutionMap> delete(String id) {
|
||||
throw new UnsupportedOperationException("Delete not supported for views");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package net.miarma.contaminus.dao.views;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.ViewSensorHistory;
|
||||
|
||||
public class ViewSensorHistoryDAO implements DataAccessObject<ViewSensorHistory, String> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public ViewSensorHistoryDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<ViewSensorHistory>> getAll() {
|
||||
Promise<List<ViewSensorHistory>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(ViewSensorHistory.class).build();
|
||||
db.execute(query, ViewSensorHistory.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorHistory> getById(String id) {
|
||||
Promise<ViewSensorHistory> promise = Promise.promise();
|
||||
ViewSensorHistory viewSensorHistory = new ViewSensorHistory();
|
||||
viewSensorHistory.setDeviceId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(ViewSensorHistory.class)
|
||||
.where(viewSensorHistory)
|
||||
.build();
|
||||
|
||||
db.execute(query, ViewSensorHistory.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorHistory> insert(ViewSensorHistory t) {
|
||||
throw new UnsupportedOperationException("Insert not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorHistory> update(ViewSensorHistory t) {
|
||||
throw new UnsupportedOperationException("Update not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorHistory> delete(String id) {
|
||||
throw new UnsupportedOperationException("Delete not supported for views");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.miarma.contaminus.dao.views;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.db.DataAccessObject;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.ViewSensorValue;
|
||||
|
||||
public class ViewSensorValueDAO implements DataAccessObject<ViewSensorValue, Integer> {
|
||||
|
||||
private final DatabaseManager db;
|
||||
|
||||
public ViewSensorValueDAO(Pool pool) {
|
||||
this.db = DatabaseManager.getInstance(pool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<ViewSensorValue>> getAll() {
|
||||
Promise<List<ViewSensorValue>> promise = Promise.promise();
|
||||
String query = QueryBuilder.select(ViewSensorValue.class).build();
|
||||
db.execute(query, ViewSensorValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? List.of() : list),
|
||||
promise::fail
|
||||
);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorValue> getById(Integer id) {
|
||||
Promise<ViewSensorValue> promise = Promise.promise();
|
||||
ViewSensorValue view = new ViewSensorValue();
|
||||
view.setSensorId(id);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(ViewSensorValue.class)
|
||||
.where(view)
|
||||
.build();
|
||||
|
||||
db.execute(query, ViewSensorValue.class,
|
||||
list -> promise.complete(list.isEmpty() ? null : list.get(0)),
|
||||
promise::fail
|
||||
);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorValue> insert(ViewSensorValue t) {
|
||||
throw new UnsupportedOperationException("Insert not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorValue> update(ViewSensorValue t) {
|
||||
throw new UnsupportedOperationException("Update not supported for views");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<ViewSensorValue> delete(Integer id) {
|
||||
throw new UnsupportedOperationException("Delete not supported for views");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package net.miarma.contaminus.database;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.RowSet;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
|
||||
public class DatabaseManager {
|
||||
private static DatabaseManager instance;
|
||||
private final JDBCPool pool;
|
||||
|
||||
private DatabaseManager(JDBCPool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public static synchronized DatabaseManager getInstance(JDBCPool pool) {
|
||||
if (instance == null) {
|
||||
instance = new DatabaseManager(pool);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Future<RowSet<Row>> testConnection() {
|
||||
return pool.query("SELECT 1").execute();
|
||||
}
|
||||
|
||||
public <T> Future<List<T>> execute(String query, Class<T> clazz,
|
||||
Handler<List<T>> onSuccess, Handler<Throwable> onFailure) {
|
||||
return pool.query(query).execute()
|
||||
.map(rows -> {
|
||||
List<T> results = new ArrayList<>();
|
||||
for (Row row : rows) {
|
||||
try {
|
||||
Constructor<T> constructor = clazz.getConstructor(Row.class);
|
||||
results.add(constructor.newInstance(row));
|
||||
} catch (NoSuchMethodException | InstantiationException |
|
||||
IllegalAccessException | InvocationTargetException e) {
|
||||
Constants.LOGGER.error("Error instantiating class: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return results;
|
||||
})
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
onSuccess.handle(ar.result());
|
||||
} else {
|
||||
onFailure.handle(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package net.miarma.contaminus.database;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
|
||||
public class QueryBuilder {
|
||||
private StringBuilder query;
|
||||
private String sort;
|
||||
private String order;
|
||||
private String limit;
|
||||
|
||||
public QueryBuilder() {
|
||||
this.query = new StringBuilder();
|
||||
}
|
||||
|
||||
private static <T> String getTableName(Class<T> clazz) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("Class cannot be null");
|
||||
}
|
||||
|
||||
if (clazz.isAnnotationPresent(Table.class)) {
|
||||
Table annotation = clazz.getAnnotation(Table.class);
|
||||
return annotation.value();
|
||||
}
|
||||
throw new IllegalArgumentException("Class does not have @Table annotation");
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder select(Class<T> clazz, String... columns) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("Class cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String tableName = getTableName(clazz);
|
||||
|
||||
qb.query.append("SELECT ");
|
||||
|
||||
if (columns.length == 0) {
|
||||
qb.query.append("* ");
|
||||
} else {
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
for (String column : columns) {
|
||||
if (column != null) {
|
||||
joiner.add(column);
|
||||
}
|
||||
}
|
||||
qb.query.append(joiner).append(" ");
|
||||
}
|
||||
|
||||
qb.query.append("FROM ").append(tableName).append(" ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder where(QueryBuilder qb, T object) {
|
||||
if (qb == null || object == null) {
|
||||
throw new IllegalArgumentException("QueryBuilder and object cannot be null");
|
||||
}
|
||||
|
||||
List<String> conditions = new ArrayList<>();
|
||||
Class<?> clazz = object.getClass();
|
||||
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object value = field.get(object);
|
||||
if (value != null) {
|
||||
if (value instanceof String) {
|
||||
conditions.add(field.getName() + " = '" + value + "'");
|
||||
} else {
|
||||
conditions.add(field.getName() + " = " + value);
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditions.isEmpty()) {
|
||||
qb.query.append("WHERE ").append(String.join(" AND ", conditions)).append(" ");
|
||||
}
|
||||
|
||||
return qb;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder select(T object, String... columns) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
Class<?> clazz = object.getClass();
|
||||
QueryBuilder qb = select(clazz, columns);
|
||||
return where(qb, object);
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder insert(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("INSERT INTO ").append(table).append(" ");
|
||||
qb.query.append("(");
|
||||
StringJoiner columns = new StringJoiner(", ");
|
||||
StringJoiner values = new StringJoiner(", ");
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
columns.add(field.getName());
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue != null) {
|
||||
if (fieldValue instanceof String) {
|
||||
values.add("'" + fieldValue + "'");
|
||||
} else {
|
||||
values.add(fieldValue.toString());
|
||||
}
|
||||
} else {
|
||||
values.add("NULL");
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
qb.query.append(columns).append(") ");
|
||||
qb.query.append("VALUES (").append(values).append(") ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder update(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("UPDATE ").append(table).append(" ");
|
||||
qb.query.append("SET ");
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue != null) {
|
||||
if (fieldValue instanceof String) {
|
||||
joiner.add(field.getName() + " = '" + fieldValue + "'");
|
||||
} else {
|
||||
joiner.add(field.getName() + " = " + fieldValue.toString());
|
||||
}
|
||||
} else {
|
||||
joiner.add(field.getName() + " = NULL");
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
qb.query.append(joiner).append(" ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public QueryBuilder orderBy(Optional<String> column, Optional<String> order) {
|
||||
column.ifPresent(c -> {
|
||||
sort = "ORDER BY " + c + " ";
|
||||
order.ifPresent(o -> {
|
||||
sort += o.equalsIgnoreCase("asc") ? "ASC" : "DESC" + " ";
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder limit(Optional<Integer> limitParam) {
|
||||
limitParam.ifPresent(param -> limit = "LIMIT " + param + " ");
|
||||
return this;
|
||||
}
|
||||
|
||||
public String build() {
|
||||
if (order != null && !order.isEmpty()) {
|
||||
query.append(order);
|
||||
}
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
query.append(sort);
|
||||
}
|
||||
if (limit != null && !limit.isEmpty()) {
|
||||
query.append(limit);
|
||||
}
|
||||
return query.toString().trim() + ";";
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.vertx.sqlclient.Row;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_co_by_device")
|
||||
public class DeviceCO {
|
||||
private Integer deviceId;
|
||||
private Float carbonMonoxide;
|
||||
private Long timestamp;
|
||||
|
||||
public DeviceCO() {}
|
||||
|
||||
public DeviceCO(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.carbonMonoxide = row.getFloat("carbonMonoxide");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DeviceCO(Integer deviceId, Float carbonMonoxide, Long timestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public Float getCarbonMonoxide() {
|
||||
return carbonMonoxide;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(carbonMonoxide, deviceId, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceCO other = (DeviceCO) obj;
|
||||
return Objects.equals(carbonMonoxide, other.carbonMonoxide) && Objects.equals(deviceId, other.deviceId)
|
||||
&& Objects.equals(timestamp, other.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceCO [deviceId=" + deviceId + ", carbonMonoxide=" + carbonMonoxide + ", timestamp=" + timestamp
|
||||
+ "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.vertx.sqlclient.Row;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_gps_by_device")
|
||||
public class DeviceGPS {
|
||||
private Integer deviceId;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
private Long timestamp;
|
||||
|
||||
public DeviceGPS() {}
|
||||
|
||||
public DeviceGPS(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.lat = row.getFloat("lat");
|
||||
this.lon = row.getFloat("lon");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DeviceGPS(Integer deviceId, Float lat, Float lon) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public Float getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public Float getLon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(deviceId, lat, lon, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceGPS other = (DeviceGPS) obj;
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(timestamp, other.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceGPS [deviceId=" + deviceId + ", lat=" + lat + ", lon=" + lon + ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.vertx.sqlclient.Row;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_weather_by_device")
|
||||
public class DeviceWeather {
|
||||
private Integer deviceId;
|
||||
private Float temperature;
|
||||
private Float humidity;
|
||||
private Long timestamp;
|
||||
|
||||
public DeviceWeather() {}
|
||||
|
||||
public DeviceWeather(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.temperature = row.getFloat("temperature");
|
||||
this.humidity = row.getFloat("humidity");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DeviceWeather(Integer deviceId, Float temperature, Float humidity, Long timestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.temperature = temperature;
|
||||
this.humidity = humidity;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public Float getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public Float getHumidity() {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(deviceId, humidity, temperature, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceWeather other = (DeviceWeather) obj;
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(humidity, other.humidity)
|
||||
&& Objects.equals(temperature, other.temperature) && Objects.equals(timestamp, other.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceWeather [deviceId=" + deviceId + ", temperature=" + temperature + ", humidity=" + humidity
|
||||
+ ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.miarma.contaminus.db;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
|
||||
public interface DataAccessObject<T, ID> {
|
||||
Future<List<T>> getAll();
|
||||
Future<T> getById(ID id);
|
||||
Future<T> insert(T t);
|
||||
Future<T> update(T t);
|
||||
Future<T> delete(ID id);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package net.miarma.contaminus.db;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.RowSet;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
|
||||
public class DatabaseManager {
|
||||
|
||||
private static DatabaseManager instance;
|
||||
private final Pool pool;
|
||||
|
||||
private DatabaseManager(Pool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public static synchronized DatabaseManager getInstance(Pool pool) {
|
||||
if (instance == null) {
|
||||
instance = new DatabaseManager(pool);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public Future<RowSet<Row>> testConnection() {
|
||||
return pool.query("SELECT 1").execute();
|
||||
}
|
||||
|
||||
public <T> Future<List<T>> execute(String query, Class<T> clazz, Handler<List<T>> onSuccess,
|
||||
Handler<Throwable> onFailure) {
|
||||
return pool.query(query).execute().map(rows -> {
|
||||
List<T> results = new ArrayList<>();
|
||||
for (Row row : rows) {
|
||||
try {
|
||||
Constructor<T> constructor = clazz.getConstructor(Row.class);
|
||||
results.add(constructor.newInstance(row));
|
||||
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException
|
||||
| InvocationTargetException e) {
|
||||
Constants.LOGGER.error("Error instantiating class: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}).onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
onSuccess.handle(ar.result());
|
||||
} else {
|
||||
onFailure.handle(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> Future<T> executeOne(String query, Class<T> clazz, Handler<T> onSuccess, Handler<Throwable> onFailure) {
|
||||
return pool.query(query).execute().map(rows -> {
|
||||
for (Row row : rows) {
|
||||
try {
|
||||
Constructor<T> constructor = clazz.getConstructor(Row.class);
|
||||
return constructor.newInstance(row);
|
||||
} catch (Exception e) {
|
||||
Constants.LOGGER.error("Error instantiating class: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return null; // Si no hay filas
|
||||
}).onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
onSuccess.handle(ar.result());
|
||||
} else {
|
||||
onFailure.handle(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.miarma.contaminus.db;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.mysqlclient.MySQLConnectOptions;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.PoolOptions;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
|
||||
public class DatabaseProvider {
|
||||
public static Pool createPool(Vertx vertx, ConfigManager config) {
|
||||
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
|
||||
.setPort(config.getIntProperty("db.port"))
|
||||
.setHost(config.getStringProperty("db.host"))
|
||||
.setDatabase(config.getStringProperty("db.name"))
|
||||
.setUser(config.getStringProperty("db.user"))
|
||||
.setPassword(config.getStringProperty("db.password"));
|
||||
|
||||
PoolOptions poolOptions = new PoolOptions().setMaxSize(10);
|
||||
return Pool.pool(vertx, connectOptions, poolOptions);
|
||||
}
|
||||
}
|
||||
357
backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java
Normal file
357
backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java
Normal file
@@ -0,0 +1,357 @@
|
||||
package net.miarma.contaminus.db;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
|
||||
|
||||
public class QueryBuilder {
|
||||
private final StringBuilder query;
|
||||
private String sort;
|
||||
private String order;
|
||||
private String limit;
|
||||
private Class<?> entityClass;
|
||||
|
||||
public QueryBuilder() {
|
||||
this.query = new StringBuilder();
|
||||
}
|
||||
|
||||
private static <T> String getTableName(Class<T> clazz) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("Class cannot be null");
|
||||
}
|
||||
|
||||
if (clazz.isAnnotationPresent(Table.class)) {
|
||||
Table annotation = clazz.getAnnotation(Table.class);
|
||||
return annotation.value();
|
||||
}
|
||||
throw new IllegalArgumentException("Class does not have @Table annotation");
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
private static Object extractValue(Object fieldValue) {
|
||||
if (fieldValue instanceof Enum<?>) {
|
||||
try {
|
||||
var method = fieldValue.getClass().getMethod("getValue");
|
||||
return method.invoke(fieldValue);
|
||||
} catch (Exception e) {
|
||||
return ((Enum<?>) fieldValue).name();
|
||||
}
|
||||
}
|
||||
return fieldValue;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder select(Class<T> clazz, String... columns) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("Class cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
qb.entityClass = clazz;
|
||||
String tableName = getTableName(clazz);
|
||||
|
||||
qb.query.append("SELECT ");
|
||||
|
||||
if (columns.length == 0) {
|
||||
qb.query.append("* ");
|
||||
} else {
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
for (String column : columns) {
|
||||
if (column != null) {
|
||||
joiner.add(column);
|
||||
}
|
||||
}
|
||||
qb.query.append(joiner).append(" ");
|
||||
}
|
||||
|
||||
qb.query.append("FROM ").append(tableName).append(" ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public QueryBuilder where(Map<String, String> filters) {
|
||||
if (filters == null || filters.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Set<String> validFields = entityClass != null
|
||||
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
|
||||
List<String> conditions = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, String> entry : filters.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
if (!validFields.contains(key)) {
|
||||
Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: " + key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("(") && value.endsWith(")")) {
|
||||
conditions.add(key + " IN " + value);
|
||||
} else if (value.matches("-?\\d+(\\.\\d+)?")) {
|
||||
conditions.add(key + " = " + value);
|
||||
} else {
|
||||
conditions.add(key + " = '" + value + "'");
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditions.isEmpty()) {
|
||||
query.append("WHERE ").append(String.join(" AND ", conditions)).append(" ");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> QueryBuilder where(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
Set<String> validFields = entityClass != null
|
||||
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
|
||||
List<String> conditions = new ArrayList<>();
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue != null) {
|
||||
String key = field.getName();
|
||||
if (!validFields.contains(key)) {
|
||||
Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: " + key);
|
||||
continue;
|
||||
}
|
||||
Object value = extractValue(fieldValue);
|
||||
if (value instanceof String || value instanceof LocalDateTime) {
|
||||
conditions.add(key + " = '" + value + "'");
|
||||
} else {
|
||||
conditions.add(key + " = " + value);
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditions.isEmpty()) {
|
||||
query.append("WHERE ").append(String.join(" AND ", conditions)).append(" ");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static <T> QueryBuilder insert(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("INSERT INTO ").append(table).append(" ");
|
||||
qb.query.append("(");
|
||||
StringJoiner columns = new StringJoiner(", ");
|
||||
StringJoiner values = new StringJoiner(", ");
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
columns.add(field.getName());
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue != null) {
|
||||
Object value = extractValue(fieldValue);
|
||||
if (value instanceof String || value instanceof LocalDateTime) {
|
||||
values.add("'" + value + "'");
|
||||
} else {
|
||||
values.add(value.toString());
|
||||
}
|
||||
} else {
|
||||
values.add("NULL");
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
qb.query.append(columns).append(") ");
|
||||
qb.query.append("VALUES (").append(values).append(") RETURNING * ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder update(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("UPDATE ").append(table).append(" SET ");
|
||||
|
||||
StringJoiner setJoiner = new StringJoiner(", ");
|
||||
StringJoiner whereJoiner = new StringJoiner(" AND ");
|
||||
|
||||
Field idField = null;
|
||||
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue == null) continue;
|
||||
|
||||
String fieldName = field.getName();
|
||||
Object value = extractValue(fieldValue);
|
||||
|
||||
if (fieldName.endsWith("Id")) {
|
||||
idField = field;
|
||||
whereJoiner.add(fieldName + " = " + (value instanceof String
|
||||
|| value instanceof LocalDateTime ? "'" + value + "'" : value));
|
||||
continue;
|
||||
}
|
||||
|
||||
setJoiner.add(fieldName + " = " + (value instanceof String
|
||||
|| value instanceof LocalDateTime ? "'" + value + "'" : value));
|
||||
} catch (Exception e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (idField == null) {
|
||||
throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
|
||||
}
|
||||
|
||||
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
|
||||
return qb;
|
||||
}
|
||||
|
||||
public static <T> QueryBuilder updateWithNulls(T object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object cannot be null");
|
||||
}
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("UPDATE ").append(table).append(" SET ");
|
||||
|
||||
StringJoiner setJoiner = new StringJoiner(", ");
|
||||
StringJoiner whereJoiner = new StringJoiner(" AND ");
|
||||
|
||||
Field idField = null;
|
||||
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
String fieldName = field.getName();
|
||||
Object fieldValue = field.get(object);
|
||||
|
||||
if (fieldName.endsWith("Id")) {
|
||||
idField = field;
|
||||
Object value = extractValue(fieldValue);
|
||||
whereJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldValue == null) {
|
||||
setJoiner.add(fieldName + " = NULL"); // ✅ esto lo borra en la BD
|
||||
} else {
|
||||
Object value = extractValue(fieldValue);
|
||||
setJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (idField == null) {
|
||||
throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
|
||||
}
|
||||
|
||||
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
|
||||
return qb;
|
||||
}
|
||||
|
||||
|
||||
public static <T> QueryBuilder delete(T object) {
|
||||
if (object == null) throw new IllegalArgumentException("Object cannot be null");
|
||||
|
||||
QueryBuilder qb = new QueryBuilder();
|
||||
String table = getTableName(object.getClass());
|
||||
qb.query.append("DELETE FROM ").append(table).append(" WHERE ");
|
||||
|
||||
StringJoiner joiner = new StringJoiner(" AND ");
|
||||
for (Field field : object.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object fieldValue = field.get(object);
|
||||
if (fieldValue != null) {
|
||||
Object value = extractValue(fieldValue);
|
||||
joiner.add(field.getName() + " = " + (value instanceof String
|
||||
|| value instanceof LocalDateTime ? "'" + value + "'" : value.toString()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Constants.LOGGER.error("(REFLECTION) Error reading field: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
qb.query.append(joiner).append(" ");
|
||||
return qb;
|
||||
}
|
||||
|
||||
public QueryBuilder orderBy(Optional<String> column, Optional<String> order) {
|
||||
column.ifPresent(c -> {
|
||||
if (entityClass != null) {
|
||||
boolean isValid = Arrays.stream(entityClass.getDeclaredFields())
|
||||
.map(Field::getName)
|
||||
.anyMatch(f -> f.equals(c));
|
||||
|
||||
if (!isValid) {
|
||||
Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en ORDER BY: " + c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sort = "ORDER BY " + c + " ";
|
||||
order.ifPresent(o -> {
|
||||
sort += o.equalsIgnoreCase("asc") ? "ASC" : "DESC" + " ";
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder limit(Optional<Integer> limitParam) {
|
||||
limitParam.ifPresent(param -> limit = "LIMIT " + param + " ");
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryBuilder offset(Optional<Integer> offsetParam) {
|
||||
offsetParam.ifPresent(param -> limit += "OFFSET " + param + " ");
|
||||
return this;
|
||||
}
|
||||
|
||||
public String build() {
|
||||
if (order != null && !order.isEmpty()) {
|
||||
query.append(order);
|
||||
}
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
query.append(sort);
|
||||
}
|
||||
if (limit != null && !limit.isEmpty()) {
|
||||
query.append(limit);
|
||||
}
|
||||
return query.toString().trim() + ";";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -10,7 +10,7 @@ import net.miarma.contaminus.util.DateParser;
|
||||
public class Actuator {
|
||||
|
||||
private Integer actuatorId;
|
||||
private Integer deviceId;
|
||||
private String deviceId;
|
||||
private Integer status;
|
||||
private Long timestamp;
|
||||
|
||||
@@ -18,12 +18,12 @@ public class Actuator {
|
||||
|
||||
public Actuator(Row row) {
|
||||
this.actuatorId = row.getInteger("actuatorId");
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.status = row.getInteger("status");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public Actuator(Integer actuatorId, Integer deviceId, Integer status, Long timestamp) {
|
||||
public Actuator(Integer actuatorId, String deviceId, Integer status, Long timestamp) {
|
||||
super();
|
||||
this.actuatorId = actuatorId;
|
||||
this.deviceId = deviceId;
|
||||
@@ -39,11 +39,11 @@ public class Actuator {
|
||||
this.actuatorId = actuatorId;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(Integer deviceId) {
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -10,6 +10,7 @@ import net.miarma.contaminus.util.DateParser;
|
||||
public class COValue {
|
||||
|
||||
private Integer valueId;
|
||||
private String deviceId;
|
||||
private Integer sensorId;
|
||||
private Float value;
|
||||
private Long timestamp;
|
||||
@@ -18,14 +19,16 @@ public class COValue {
|
||||
|
||||
public COValue(Row row) {
|
||||
this.valueId = row.getInteger("valueId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.value = row.getFloat("value");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public COValue(Integer valueId, Integer sensorId, Float value, Long timestamp) {
|
||||
public COValue(Integer valueId, String deviceId, Integer sensorId, Float value, Long timestamp) {
|
||||
super();
|
||||
this.valueId = valueId;
|
||||
this.deviceId = deviceId;
|
||||
this.sensorId = sensorId;
|
||||
this.value = value;
|
||||
this.timestamp = timestamp;
|
||||
@@ -39,6 +42,14 @@ public class COValue {
|
||||
this.valueId = valueId;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public Integer getSensorId() {
|
||||
return sensorId;
|
||||
}
|
||||
@@ -65,7 +76,7 @@ public class COValue {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sensorId, timestamp, value, valueId);
|
||||
return Objects.hash(deviceId, sensorId, timestamp, value, valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,17 +88,20 @@ public class COValue {
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
COValue other = (COValue) obj;
|
||||
return Objects.equals(sensorId, other.sensorId) && Objects.equals(timestamp, other.timestamp)
|
||||
&& Objects.equals(value, other.value) && Objects.equals(valueId, other.valueId);
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(sensorId, other.sensorId)
|
||||
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(value, other.value)
|
||||
&& Objects.equals(valueId, other.valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "COValue [valueId=" + valueId + ", sensorId=" + sensorId + ", value=" + value + ", timestamp="
|
||||
+ timestamp + "]";
|
||||
return "COValue [valueId=" + valueId + ", deviceId=" + deviceId + ", sensorId=" + sensorId + ", value=" + value
|
||||
+ ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
public static COValue fromPayload(DevicePayload payload) {
|
||||
return new COValue(null, payload.getDeviceId(), payload.getSensorId(), payload.getCarbonMonoxide(), payload.getTimestamp());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import io.vertx.sqlclient.Row;
|
||||
import net.miarma.contaminus.common.Table;
|
||||
@@ -6,30 +6,34 @@ import net.miarma.contaminus.common.Table;
|
||||
@Table("devices")
|
||||
public class Device {
|
||||
|
||||
private Integer deviceId;
|
||||
private String deviceId;
|
||||
private Integer groupId;
|
||||
private String deviceName;
|
||||
private Integer deviceRole;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.groupId = row.getInteger("groupId");
|
||||
this.deviceName = row.getString("deviceName");
|
||||
this.deviceRole = row.getInteger("deviceRole");
|
||||
}
|
||||
|
||||
public Device(Integer deviceId, Integer groupId, String deviceName) {
|
||||
public Device(String deviceId, Integer groupId, String deviceName, Integer deviceRole) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.groupId = groupId;
|
||||
this.deviceName = deviceName;
|
||||
this.deviceRole = deviceRole;
|
||||
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(Integer deviceId) {
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@@ -49,9 +53,13 @@ public class Device {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public Integer getDeviceRole() {
|
||||
return deviceRole;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setDevice(Integer deviceRole) {
|
||||
this.deviceRole = deviceRole;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class DevicePayload {
|
||||
private String deviceId;
|
||||
private Integer sensorId;
|
||||
private Float temperature;
|
||||
private Float humidity;
|
||||
private Float pressure;
|
||||
private Float carbonMonoxide;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
private Long timestamp;
|
||||
|
||||
public DevicePayload() {}
|
||||
|
||||
public DevicePayload(String deviceId, Integer sensorId, String sensorType, String unit, Integer sensorStatus,
|
||||
Float temperature, Float humidity, Float pressure, Float carbonMonoxide, Float lat, Float lon, Long timestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.sensorId = sensorId;
|
||||
this.temperature = temperature;
|
||||
this.humidity = humidity;
|
||||
this.pressure = pressure;
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public Integer getSensorId() {
|
||||
return sensorId;
|
||||
}
|
||||
|
||||
public void setSensorId(Integer sensorId) {
|
||||
this.sensorId = sensorId;
|
||||
}
|
||||
|
||||
public Float getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public void setTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public Float getHumidity() {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public void setHumidity(Float humidity) {
|
||||
this.humidity = humidity;
|
||||
}
|
||||
|
||||
public Float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public Float getCarbonMonoxide() {
|
||||
return carbonMonoxide;
|
||||
}
|
||||
|
||||
public void setCarbonMonoxide(Float carbonMonoxide) {
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
}
|
||||
|
||||
public Float getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public void setLat(Float lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public Float getLon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
public void setLon(Float lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(carbonMonoxide, deviceId, humidity, lat, lon, pressure, sensorId, temperature, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DevicePayload other = (DevicePayload) obj;
|
||||
return Objects.equals(carbonMonoxide, other.carbonMonoxide) && Objects.equals(deviceId, other.deviceId)
|
||||
&& Objects.equals(humidity, other.humidity) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(pressure, other.pressure)
|
||||
&& Objects.equals(sensorId, other.sensorId) && Objects.equals(temperature, other.temperature)
|
||||
&& Objects.equals(timestamp, other.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DevicePayload [deviceId=" + deviceId + ", sensorId=" + sensorId + ", temperature=" + temperature
|
||||
+ ", humidity=" + humidity + ", pressure=" + pressure + ", carbonMonoxide=" + carbonMonoxide + ", lat="
|
||||
+ lat + ", lon=" + lon + ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -10,6 +10,7 @@ import net.miarma.contaminus.util.DateParser;
|
||||
public class GpsValue {
|
||||
|
||||
private Integer valueId;
|
||||
private String deviceId;
|
||||
private Integer sensorId;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
@@ -19,15 +20,17 @@ public class GpsValue {
|
||||
|
||||
public GpsValue(Row row) {
|
||||
this.valueId = row.getInteger("valueId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.lat = row.getFloat("lat");
|
||||
this.lon = row.getFloat("lon");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public GpsValue(Integer valueId, Integer sensorId, Float lat, Float lon, Long timestamp) {
|
||||
public GpsValue(Integer valueId, String deviceId, Integer sensorId, Float lat, Float lon, Long timestamp) {
|
||||
super();
|
||||
this.valueId = valueId;
|
||||
this.deviceId = deviceId;
|
||||
this.sensorId = sensorId;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
@@ -42,6 +45,14 @@ public class GpsValue {
|
||||
this.valueId = valueId;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public Integer getSensorId() {
|
||||
return sensorId;
|
||||
}
|
||||
@@ -76,7 +87,7 @@ public class GpsValue {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(lat, lon, sensorId, timestamp, valueId);
|
||||
return Objects.hash(deviceId, lat, lon, sensorId, timestamp, valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,17 +99,20 @@ public class GpsValue {
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
GpsValue other = (GpsValue) obj;
|
||||
return Objects.equals(lat, other.lat) && Objects.equals(lon, other.lon)
|
||||
&& Objects.equals(sensorId, other.sensorId) && Objects.equals(timestamp, other.timestamp)
|
||||
&& Objects.equals(valueId, other.valueId);
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(sensorId, other.sensorId)
|
||||
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(valueId, other.valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GpsValue [valueId=" + valueId + ", sensorId=" + sensorId + ", lat=" + lat + ", lon=" + lon
|
||||
+ ", timestamp=" + timestamp + "]";
|
||||
return "GpsValue [valueId=" + valueId + ", deviceId=" + deviceId + ", sensorId=" + sensorId + ", lat=" + lat
|
||||
+ ", lon=" + lon + ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
public static GpsValue fromPayload(DevicePayload payload) {
|
||||
return new GpsValue(null, payload.getDeviceId(), payload.getSensorId(), payload.getLat(), payload.getLon(),
|
||||
payload.getTimestamp());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -10,7 +10,7 @@ import net.miarma.contaminus.util.DateParser;
|
||||
public class Sensor {
|
||||
|
||||
private Integer sensorId;
|
||||
private Integer deviceId;
|
||||
private String deviceId;
|
||||
private String sensorType;
|
||||
private String unit;
|
||||
private Integer status;
|
||||
@@ -20,14 +20,14 @@ public class Sensor {
|
||||
|
||||
public Sensor(Row row) {
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorType = row.getString("sensorType");
|
||||
this.unit = row.getString("unit");
|
||||
this.status = row.getInteger("status");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public Sensor(Integer sensorId, Integer deviceId, String sensorType, String unit, Integer status, Long timestamp) {
|
||||
public Sensor(Integer sensorId, String deviceId, String sensorType, String unit, Integer status, Long timestamp) {
|
||||
super();
|
||||
this.sensorId = sensorId;
|
||||
this.deviceId = deviceId;
|
||||
@@ -45,11 +45,11 @@ public class Sensor {
|
||||
this.sensorId = sensorId;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(Integer deviceId) {
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.vertx.sqlclient.Row;
|
||||
@@ -6,8 +6,9 @@ import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_latest_values")
|
||||
public class DeviceLatestValuesView {
|
||||
private Integer deviceId;
|
||||
public class ViewLatestValues {
|
||||
|
||||
private String deviceId;
|
||||
private Integer sensorId;
|
||||
private String sensorType;
|
||||
private String unit;
|
||||
@@ -15,15 +16,16 @@ public class DeviceLatestValuesView {
|
||||
private Long sensorTimestamp;
|
||||
private Float temperature;
|
||||
private Float humidity;
|
||||
private Float pressure;
|
||||
private Float carbonMonoxide;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
private Long airValuesTimestamp;
|
||||
|
||||
public DeviceLatestValuesView() {}
|
||||
public ViewLatestValues() {}
|
||||
|
||||
public DeviceLatestValuesView(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
public ViewLatestValues(Row row) {
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.sensorType = row.getString("sensorType");
|
||||
this.unit = row.getString("unit");
|
||||
@@ -31,14 +33,15 @@ public class DeviceLatestValuesView {
|
||||
this.sensorTimestamp = DateParser.parseDate(row.getLocalDateTime("sensorTimestamp"));
|
||||
this.temperature = row.getFloat("temperature");
|
||||
this.humidity = row.getFloat("humidity");
|
||||
this.pressure = row.getFloat("pressure");
|
||||
this.carbonMonoxide = row.getFloat("carbonMonoxide");
|
||||
this.lat = row.getFloat("lat");
|
||||
this.lon = row.getFloat("lon");
|
||||
this.airValuesTimestamp = DateParser.parseDate(row.getLocalDateTime("airValuesTimestamp"));
|
||||
}
|
||||
|
||||
public DeviceLatestValuesView(Integer deviceId, Integer sensorId, String sensorType, String unit, Integer sensorStatus,
|
||||
Long sensorTimestamp, Float temperature, Float humidity, Float carbonMonoxide, Float lat, Float lon,
|
||||
public ViewLatestValues(String deviceId, Integer sensorId, String sensorType, String unit, Integer sensorStatus,
|
||||
Long sensorTimestamp, Float temperature, Float humidity, Float pressure, Float carbonMonoxide, Float lat, Float lon,
|
||||
Long airValuesTimestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
@@ -55,7 +58,7 @@ public class DeviceLatestValuesView {
|
||||
this.airValuesTimestamp = airValuesTimestamp;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@@ -87,6 +90,10 @@ public class DeviceLatestValuesView {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public Float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public Float getCarbonMonoxide() {
|
||||
return carbonMonoxide;
|
||||
}
|
||||
@@ -103,10 +110,62 @@ public class DeviceLatestValuesView {
|
||||
return airValuesTimestamp;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public void setSensorId(Integer sensorId) {
|
||||
this.sensorId = sensorId;
|
||||
}
|
||||
|
||||
public void setSensorType(String sensorType) {
|
||||
this.sensorType = sensorType;
|
||||
}
|
||||
|
||||
public void setUnit(String unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public void setSensorStatus(Integer sensorStatus) {
|
||||
this.sensorStatus = sensorStatus;
|
||||
}
|
||||
|
||||
public void setSensorTimestamp(Long sensorTimestamp) {
|
||||
this.sensorTimestamp = sensorTimestamp;
|
||||
}
|
||||
|
||||
public void setTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public void setHumidity(Float humidity) {
|
||||
this.humidity = humidity;
|
||||
}
|
||||
|
||||
public void setPressure(Float pressure) {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public void setCarbonMonoxide(Float carbonMonoxide) {
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
}
|
||||
|
||||
public void setLat(Float lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public void setLon(Float lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public void setAirValuesTimestamp(Long airValuesTimestamp) {
|
||||
this.airValuesTimestamp = airValuesTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(airValuesTimestamp, carbonMonoxide, deviceId, humidity, lat, lon, sensorId, sensorStatus,
|
||||
sensorTimestamp, sensorType, temperature, unit);
|
||||
return Objects.hash(airValuesTimestamp, carbonMonoxide, deviceId, humidity, lat, lon, pressure, sensorId,
|
||||
sensorStatus, sensorTimestamp, sensorType, temperature, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,12 +176,12 @@ public class DeviceLatestValuesView {
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceLatestValuesView other = (DeviceLatestValuesView) obj;
|
||||
ViewLatestValues other = (ViewLatestValues) obj;
|
||||
return Objects.equals(airValuesTimestamp, other.airValuesTimestamp)
|
||||
&& Objects.equals(carbonMonoxide, other.carbonMonoxide) && Objects.equals(deviceId, other.deviceId)
|
||||
&& Objects.equals(humidity, other.humidity) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(sensorId, other.sensorId)
|
||||
&& Objects.equals(sensorStatus, other.sensorStatus)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(pressure, other.pressure)
|
||||
&& Objects.equals(sensorId, other.sensorId) && Objects.equals(sensorStatus, other.sensorStatus)
|
||||
&& Objects.equals(sensorTimestamp, other.sensorTimestamp)
|
||||
&& Objects.equals(sensorType, other.sensorType) && Objects.equals(temperature, other.temperature)
|
||||
&& Objects.equals(unit, other.unit);
|
||||
@@ -130,13 +189,13 @@ public class DeviceLatestValuesView {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceLatestValuesView [deviceId=" + deviceId + ", sensorId=" + sensorId + ", sensorType=" + sensorType
|
||||
return "ViewLatestValues [deviceId=" + deviceId + ", sensorId=" + sensorId + ", sensorType=" + sensorType
|
||||
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", sensorTimestamp=" + sensorTimestamp
|
||||
+ ", temperature=" + temperature + ", humidity=" + humidity + ", carbonMonoxide=" + carbonMonoxide
|
||||
+ ", lat=" + lat + ", lon=" + lon + ", airValuesTimestamp=" + airValuesTimestamp + "]";
|
||||
+ ", temperature=" + temperature + ", humidity=" + humidity + ", pressure=" + pressure
|
||||
+ ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon=" + lon + ", airValuesTimestamp="
|
||||
+ airValuesTimestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -7,18 +7,18 @@ import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_pollution_map")
|
||||
public class DevicePollutionMap {
|
||||
private Integer deviceId;
|
||||
public class ViewPollutionMap {
|
||||
private String deviceId;
|
||||
private String deviceName;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
private Float carbonMonoxide;
|
||||
private Long timestamp;
|
||||
|
||||
public DevicePollutionMap() {}
|
||||
public ViewPollutionMap() {}
|
||||
|
||||
public DevicePollutionMap(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
public ViewPollutionMap(Row row) {
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.deviceName = row.getString("deviceName");
|
||||
this.lat = row.getFloat("lat");
|
||||
this.lon = row.getFloat("lon");
|
||||
@@ -26,7 +26,7 @@ public class DevicePollutionMap {
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DevicePollutionMap(Integer deviceId, String deviceName, Float lat, Float lon, Float carbonMonoxide,
|
||||
public ViewPollutionMap(String deviceId, String deviceName, Float lat, Float lon, Float carbonMonoxide,
|
||||
Long timestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
@@ -37,7 +37,7 @@ public class DevicePollutionMap {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,32 @@ public class DevicePollutionMap {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public void setLat(Float lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public void setLon(Float lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public void setCarbonMonoxide(Float carbonMonoxide) {
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(carbonMonoxide, deviceId, deviceName, lat, lon, timestamp);
|
||||
@@ -74,7 +100,7 @@ public class DevicePollutionMap {
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DevicePollutionMap other = (DevicePollutionMap) obj;
|
||||
ViewPollutionMap other = (ViewPollutionMap) obj;
|
||||
return Objects.equals(carbonMonoxide, other.carbonMonoxide) && Objects.equals(deviceId, other.deviceId)
|
||||
&& Objects.equals(deviceName, other.deviceName) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(timestamp, other.timestamp);
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -7,24 +7,24 @@ import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_sensor_history_by_device")
|
||||
public class DeviceSensorHistory {
|
||||
private Integer deviceId;
|
||||
public class ViewSensorHistory {
|
||||
private String deviceId;
|
||||
private String deviceName;
|
||||
private Float value;
|
||||
private String valueType;
|
||||
private Long timestamp;
|
||||
|
||||
public DeviceSensorHistory() {}
|
||||
public ViewSensorHistory() {}
|
||||
|
||||
public DeviceSensorHistory(Row row) {
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
public ViewSensorHistory(Row row) {
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.deviceName = row.getString("deviceName");
|
||||
this.value = row.getFloat("value");
|
||||
this.valueType = row.getString("valueType");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DeviceSensorHistory(Integer deviceId, String deviceName, Float value, String valueType, Long timestamp) {
|
||||
public ViewSensorHistory(String deviceId, String deviceName, Float value, String valueType, Long timestamp) {
|
||||
super();
|
||||
this.deviceId = deviceId;
|
||||
this.deviceName = deviceName;
|
||||
@@ -33,7 +33,7 @@ public class DeviceSensorHistory {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,28 @@ public class DeviceSensorHistory {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public void setValue(Float value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void setValueType(String valueType) {
|
||||
this.valueType = valueType;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(deviceId, deviceName, timestamp, value, valueType);
|
||||
@@ -66,7 +88,7 @@ public class DeviceSensorHistory {
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceSensorHistory other = (DeviceSensorHistory) obj;
|
||||
ViewSensorHistory other = (ViewSensorHistory) obj;
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(deviceName, other.deviceName)
|
||||
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(value, other.value)
|
||||
&& Objects.equals(valueType, other.valueType);
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -7,37 +7,39 @@ import net.miarma.contaminus.common.Table;
|
||||
import net.miarma.contaminus.util.DateParser;
|
||||
|
||||
@Table("v_sensor_values")
|
||||
public class DeviceSensorValue {
|
||||
public class ViewSensorValue {
|
||||
private Integer sensorId;
|
||||
private Integer deviceId;
|
||||
private String deviceId;
|
||||
private String sensorType;
|
||||
private String unit;
|
||||
private Integer sensorStatus;
|
||||
private Float temperature;
|
||||
private Float humidity;
|
||||
private Float pressure;
|
||||
private Float carbonMonoxide;
|
||||
private Float lat;
|
||||
private Float lon;
|
||||
private Long timestamp;
|
||||
|
||||
public DeviceSensorValue() {}
|
||||
public ViewSensorValue() {}
|
||||
|
||||
public DeviceSensorValue(Row row) {
|
||||
public ViewSensorValue(Row row) {
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.deviceId = row.getInteger("deviceId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorType = row.getString("sensorType");
|
||||
this.unit = row.getString("unit");
|
||||
this.sensorStatus = row.getInteger("sensorStatus");
|
||||
this.temperature = row.getFloat("temperature");
|
||||
this.humidity = row.getFloat("humidity");
|
||||
this.pressure = row.getFloat("pressure");
|
||||
this.carbonMonoxide = row.getFloat("carbonMonoxide");
|
||||
this.lat = row.getFloat("lat");
|
||||
this.lon = row.getFloat("lon");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public DeviceSensorValue(Integer sensorId, Integer deviceId, String sensorType, String unit, Integer sensorStatus,
|
||||
Float temperature, Float humidity, Float carbonMonoxide, Float lat, Float lon, Long timestamp) {
|
||||
public ViewSensorValue(Integer sensorId, String deviceId, String sensorType, String unit, Integer sensorStatus,
|
||||
Float temperature, Float humidity, Float pressure, Float carbonMonoxide, Float lat, Float lon, Long timestamp) {
|
||||
super();
|
||||
this.sensorId = sensorId;
|
||||
this.deviceId = deviceId;
|
||||
@@ -46,6 +48,7 @@ public class DeviceSensorValue {
|
||||
this.sensorStatus = sensorStatus;
|
||||
this.temperature = temperature;
|
||||
this.humidity = humidity;
|
||||
this.pressure = pressure;
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
@@ -56,7 +59,7 @@ public class DeviceSensorValue {
|
||||
return sensorId;
|
||||
}
|
||||
|
||||
public Integer getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@@ -80,6 +83,10 @@ public class DeviceSensorValue {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public Float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public Float getCarbonMonoxide() {
|
||||
return carbonMonoxide;
|
||||
}
|
||||
@@ -96,9 +103,59 @@ public class DeviceSensorValue {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setSensorId(Integer sensorId) {
|
||||
this.sensorId = sensorId;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public void setSensorType(String sensorType) {
|
||||
this.sensorType = sensorType;
|
||||
}
|
||||
|
||||
public void setUnit(String unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public void setSensorStatus(Integer sensorStatus) {
|
||||
this.sensorStatus = sensorStatus;
|
||||
}
|
||||
|
||||
public void setTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public void setHumidity(Float humidity) {
|
||||
this.humidity = humidity;
|
||||
}
|
||||
|
||||
public void setPressure(Float pressure) {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public void setCarbonMonoxide(Float carbonMonoxide) {
|
||||
this.carbonMonoxide = carbonMonoxide;
|
||||
}
|
||||
|
||||
public void setLat(Float lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public void setLon(Float lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(carbonMonoxide, deviceId, humidity, lat, lon, sensorId, sensorStatus, sensorType,
|
||||
return Objects.hash(carbonMonoxide, deviceId, humidity, lat, lon, pressure, sensorId, sensorStatus, sensorType,
|
||||
temperature, timestamp, unit);
|
||||
}
|
||||
|
||||
@@ -110,22 +167,23 @@ public class DeviceSensorValue {
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DeviceSensorValue other = (DeviceSensorValue) obj;
|
||||
ViewSensorValue other = (ViewSensorValue) obj;
|
||||
return Objects.equals(carbonMonoxide, other.carbonMonoxide) && Objects.equals(deviceId, other.deviceId)
|
||||
&& Objects.equals(humidity, other.humidity) && Objects.equals(lat, other.lat)
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(sensorId, other.sensorId)
|
||||
&& Objects.equals(sensorStatus, other.sensorStatus) && Objects.equals(sensorType, other.sensorType)
|
||||
&& Objects.equals(temperature, other.temperature) && Objects.equals(timestamp, other.timestamp)
|
||||
&& Objects.equals(unit, other.unit);
|
||||
&& Objects.equals(lon, other.lon) && Objects.equals(pressure, other.pressure)
|
||||
&& Objects.equals(sensorId, other.sensorId) && Objects.equals(sensorStatus, other.sensorStatus)
|
||||
&& Objects.equals(sensorType, other.sensorType) && Objects.equals(temperature, other.temperature)
|
||||
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(unit, other.unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceSensorValue [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType
|
||||
return "ViewSensorValue [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType
|
||||
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", temperature=" + temperature + ", humidity="
|
||||
+ humidity + ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon=" + lon + ", timestamp="
|
||||
+ timestamp + "]";
|
||||
+ humidity + ", pressure=" + pressure + ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon="
|
||||
+ lon + ", timestamp=" + timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.database.entities;
|
||||
package net.miarma.contaminus.entities;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -10,27 +10,33 @@ import net.miarma.contaminus.util.DateParser;
|
||||
public class WeatherValue {
|
||||
|
||||
private Integer valueId;
|
||||
private String deviceId;
|
||||
private Integer sensorId;
|
||||
private Float temperature;
|
||||
private Float humidity;
|
||||
private Float pressure;
|
||||
private Long timestamp;
|
||||
|
||||
public WeatherValue() {}
|
||||
|
||||
public WeatherValue(Row row) {
|
||||
this.valueId = row.getInteger("valueId");
|
||||
this.deviceId = row.getString("deviceId");
|
||||
this.sensorId = row.getInteger("sensorId");
|
||||
this.temperature = row.getFloat("temperature");
|
||||
this.humidity = row.getFloat("humidity");
|
||||
this.pressure = row.getFloat("pressure");
|
||||
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
|
||||
}
|
||||
|
||||
public WeatherValue(Integer valueId, Integer sensorId, Float temperature, Float humidity, Long timestamp) {
|
||||
public WeatherValue(Integer valueId, String deviceId, Integer sensorId, Float temperature, Float humidity, Float pressure, Long timestamp) {
|
||||
super();
|
||||
this.valueId = valueId;
|
||||
this.deviceId = deviceId;
|
||||
this.sensorId = sensorId;
|
||||
this.temperature = temperature;
|
||||
this.humidity = humidity;
|
||||
this.pressure = pressure;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@@ -42,6 +48,14 @@ public class WeatherValue {
|
||||
this.valueId = valueId;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(String deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public Integer getSensorId() {
|
||||
return sensorId;
|
||||
}
|
||||
@@ -66,6 +80,14 @@ public class WeatherValue {
|
||||
this.humidity = humidity;
|
||||
}
|
||||
|
||||
public Float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public void setPressure(Float pressure) {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
@@ -76,7 +98,7 @@ public class WeatherValue {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(humidity, sensorId, temperature, timestamp, valueId);
|
||||
return Objects.hash(deviceId, humidity, pressure, sensorId, temperature, timestamp, valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,17 +110,22 @@ public class WeatherValue {
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
WeatherValue other = (WeatherValue) obj;
|
||||
return Objects.equals(humidity, other.humidity) && Objects.equals(sensorId, other.sensorId)
|
||||
return Objects.equals(deviceId, other.deviceId) && Objects.equals(humidity, other.humidity)
|
||||
&& Objects.equals(pressure, other.pressure) && Objects.equals(sensorId, other.sensorId)
|
||||
&& Objects.equals(temperature, other.temperature) && Objects.equals(timestamp, other.timestamp)
|
||||
&& Objects.equals(valueId, other.valueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WeatherValue [valueId=" + valueId + ", sensorId=" + sensorId + ", temperature=" + temperature
|
||||
+ ", humidity=" + humidity + ", timestamp=" + timestamp + "]";
|
||||
return "WeatherValue [valueId=" + valueId + ", deviceId=" + deviceId + ", sensorId=" + sensorId
|
||||
+ ", temperature=" + temperature + ", humidity=" + humidity + ", pressure=" + pressure + ", timestamp="
|
||||
+ timestamp + "]";
|
||||
}
|
||||
|
||||
|
||||
public static WeatherValue fromPayload(DevicePayload payload) {
|
||||
return new WeatherValue(null, payload.getDeviceId(), payload.getSensorId(), payload.getTemperature(),
|
||||
payload.getHumidity(), payload.getPressure(), payload.getTimestamp());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
package net.miarma.contaminus.server;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpMethod;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import io.vertx.ext.web.handler.CorsHandler;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.common.SingleJsonResponse;
|
||||
import net.miarma.contaminus.database.DatabaseManager;
|
||||
import net.miarma.contaminus.database.QueryBuilder;
|
||||
import net.miarma.contaminus.database.entities.Actuator;
|
||||
import net.miarma.contaminus.database.entities.Device;
|
||||
import net.miarma.contaminus.database.entities.DeviceLatestValuesView;
|
||||
import net.miarma.contaminus.database.entities.DevicePollutionMap;
|
||||
import net.miarma.contaminus.database.entities.DeviceSensorHistory;
|
||||
import net.miarma.contaminus.database.entities.DeviceSensorValue;
|
||||
import net.miarma.contaminus.database.entities.Group;
|
||||
import net.miarma.contaminus.database.entities.Sensor;
|
||||
|
||||
/*
|
||||
* This class is a Verticle that will handle the Data Layer API.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DataLayerAPIVerticle extends AbstractVerticle {
|
||||
private JDBCPool pool;
|
||||
private DatabaseManager dbManager;
|
||||
private ConfigManager configManager;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public DataLayerAPIVerticle() {
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
String jdbcUrl = configManager.getJdbcUrl();
|
||||
String dbUser = configManager.getStringProperty("db.user");
|
||||
String dbPwd = configManager.getStringProperty("db.pwd");
|
||||
Integer poolSize = configManager.getIntProperty("db.poolSize");
|
||||
|
||||
JsonObject dbConfig = new JsonObject()
|
||||
.put("url", jdbcUrl)
|
||||
.put("user", dbUser)
|
||||
.put("password", dbPwd)
|
||||
.put("max_pool_size", poolSize != null ? poolSize : 10);
|
||||
|
||||
this.pool = JDBCPool.pool(Vertx.vertx(), dbConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
Constants.LOGGER.info("📡 Iniciando DataLayerAPIVerticle...");
|
||||
|
||||
dbManager = DatabaseManager.getInstance(pool);
|
||||
|
||||
Router router = Router.router(vertx);
|
||||
Set<HttpMethod> allowedMethods = new HashSet<>(
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.OPTIONS)); // Por ejemplo
|
||||
Set<String> allowedHeaders = new HashSet<>(Arrays.asList("Content-Type", "Authorization"));
|
||||
|
||||
router.route().handler(CorsHandler.create()
|
||||
.allowCredentials(true)
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowedMethods(allowedMethods));
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
// Group Routes
|
||||
router.route(HttpMethod.GET, Constants.GET_GROUPS).handler(this::getAllGroups);
|
||||
router.route(HttpMethod.GET, Constants.GET_GROUP_BY_ID).handler(this::getGroupById);
|
||||
router.route(HttpMethod.POST, Constants.POST_GROUPS).handler(this::addGroup);
|
||||
router.route(HttpMethod.PUT, Constants.PUT_GROUP_BY_ID).handler(this::updateGroup);
|
||||
|
||||
// Device Routes
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICES).handler(this::getAllDevices);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_BY_ID).handler(this::getDeviceById);
|
||||
router.route(HttpMethod.POST, Constants.POST_DEVICES).handler(this::addDevice);
|
||||
router.route(HttpMethod.PUT, Constants.PUT_DEVICE_BY_ID).handler(this::updateDevice);
|
||||
|
||||
// Sensor Routes
|
||||
router.route(HttpMethod.GET, Constants.GET_SENSORS).handler(this::getAllSensors);
|
||||
router.route(HttpMethod.GET, Constants.GET_SENSOR_BY_ID).handler(this::getSensorById);
|
||||
router.route(HttpMethod.POST, Constants.POST_SENSORS).handler(this::addSensor);
|
||||
router.route(HttpMethod.PUT, Constants.PUT_SENSOR_BY_ID).handler(this::updateSensor);
|
||||
|
||||
// Actuator Routes
|
||||
router.route(HttpMethod.GET, Constants.GET_ACTUATORS).handler(this::getAllActuators);
|
||||
router.route(HttpMethod.GET, Constants.GET_ACTUATOR_BY_ID).handler(this::getActuatorById);
|
||||
router.route(HttpMethod.POST, Constants.POST_ACTUATORS).handler(this::addActuator);
|
||||
router.route(HttpMethod.PUT, Constants.PUT_ACTUATOR_BY_ID).handler(this::updateActuator);
|
||||
|
||||
// Views Routes
|
||||
router.route(HttpMethod.GET, Constants.GET_LATEST_VALUES_VIEW).handler(this::getLatestValuesView);
|
||||
router.route(HttpMethod.GET, Constants.GET_POLLUTION_MAP_VIEW).handler(this::getDevicePollutionMapView);
|
||||
router.route(HttpMethod.GET, Constants.GET_SENSOR_VALUES_VIEW).handler(this::getSensorValuesView);
|
||||
router.route(HttpMethod.GET, Constants.GET_SENSOR_HISTORY_BY_DEVICE_VIEW).handler(this::getSensorHistoryByDeviceView);
|
||||
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getDataApiPort(), configManager.getHost());
|
||||
|
||||
pool.query("SELECT 1").execute(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 Connected to DB");
|
||||
startPromise.complete();
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 Failed to connect to DB: " + ar.cause());
|
||||
startPromise.fail(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllGroups(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(Group.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Group.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getGroupById(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
Group group = new Group(groupId, null);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(group)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Group.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void addGroup(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Group group = gson.fromJson(body.toString(), Group.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.insert(group)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Group.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Group added successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGroup(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Group group = gson.fromJson(body.toString(), Group.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.update(group)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Group.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Group updated successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllDevices(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(Device.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Device.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getDeviceById(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
Device device = new Device(deviceId, null, null);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(device)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Device.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void addDevice(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Device device = gson.fromJson(body.toString(), Device.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.insert(device)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Device.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Device added successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDevice(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Device device = gson.fromJson(body.toString(), Device.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.update(device)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Device.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Device updated successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllSensors(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(Sensor.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Sensor.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorById(RoutingContext context) {
|
||||
Integer sensorId = Integer.parseInt(context.request().getParam("sensorId"));
|
||||
Sensor sensor = new Sensor(sensorId, null, null, null, null, null);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(sensor)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Sensor.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void addSensor(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Sensor sensor = gson.fromJson(body.toString(), Sensor.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.insert(sensor)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Sensor.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Sensor added successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSensor(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Sensor sensor = gson.fromJson(body.toString(), Sensor.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.update(sensor)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Sensor.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Sensor updated successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllActuators(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(Actuator.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Actuator.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getActuatorById(RoutingContext context) {
|
||||
Integer actuatorId = Integer.parseInt(context.request().getParam("actuatorId"));
|
||||
Actuator actuator = new Actuator(actuatorId, null, null, null);
|
||||
|
||||
String query = QueryBuilder
|
||||
.select(actuator)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Actuator.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void addActuator(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Actuator actuator = gson.fromJson(body.toString(), Actuator.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.insert(actuator)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Actuator.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Actuator added successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateActuator(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Actuator actuator = gson.fromJson(body.toString(), Actuator.class);
|
||||
|
||||
String query = QueryBuilder
|
||||
.update(actuator)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, Actuator.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(SingleJsonResponse.of("Actuator updated successfully")));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getLatestValuesView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(DeviceLatestValuesView.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, DeviceLatestValuesView.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getDevicePollutionMapView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(DevicePollutionMap.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, DevicePollutionMap.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorValuesView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(DeviceSensorValue.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, DeviceSensorValue.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorHistoryByDeviceView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(DeviceSensorHistory.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, DeviceSensorHistory.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package net.miarma.contaminus.server;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpMethod;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientOptions;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import io.vertx.ext.web.handler.CorsHandler;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.database.entities.Actuator;
|
||||
import net.miarma.contaminus.database.entities.Device;
|
||||
import net.miarma.contaminus.database.entities.DeviceLatestValuesView;
|
||||
import net.miarma.contaminus.database.entities.DevicePollutionMap;
|
||||
import net.miarma.contaminus.database.entities.DeviceSensorHistory;
|
||||
import net.miarma.contaminus.database.entities.DeviceSensorValue;
|
||||
import net.miarma.contaminus.database.entities.Sensor;
|
||||
import net.miarma.contaminus.util.RestClientUtil;
|
||||
|
||||
public class LogicLayerAPIVerticle extends AbstractVerticle {
|
||||
private ConfigManager configManager;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
private RestClientUtil restClient;
|
||||
|
||||
public LogicLayerAPIVerticle() {
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
WebClientOptions options = new WebClientOptions()
|
||||
.setUserAgent("ContaminUS");
|
||||
this.restClient = new RestClientUtil(WebClient.create(Vertx.vertx(), options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
Constants.LOGGER.info("📡 Iniciando LogicApiVerticle...");
|
||||
|
||||
Router router = Router.router(vertx);
|
||||
Set<HttpMethod> allowedMethods = new HashSet<>(
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.OPTIONS)); // Por ejemplo
|
||||
Set<String> allowedHeaders = new HashSet<>(Arrays.asList("Content-Type", "Authorization"));
|
||||
|
||||
router.route().handler(CorsHandler.create()
|
||||
.allowCredentials(true)
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowedMethods(allowedMethods));
|
||||
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
router.route(HttpMethod.GET, Constants.GET_GROUP_DEVICES).handler(this::getGroupDevices);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_SENSORS).handler(this::getDeviceSensors);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_ACTUATORS).handler(this::getDeviceActuators);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_LATEST_VALUES).handler(this::getDeviceLatestValues);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_POLLUTION_MAP).handler(this::getDevicePollutionMap);
|
||||
router.route(HttpMethod.GET, Constants.GET_DEVICE_HISTORY).handler(this::getDeviceHistory);
|
||||
router.route(HttpMethod.GET, Constants.GET_SENSOR_VALUES).handler(this::getSensorValues);
|
||||
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getLogicApiPort(), configManager.getHost());
|
||||
|
||||
startPromise.complete();
|
||||
}
|
||||
|
||||
private void getGroupDevices(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
|
||||
Promise<Device[]> resultList = Promise.promise();
|
||||
resultList.future().onComplete(complete -> {
|
||||
if(complete.succeeded()) {
|
||||
List<Device> aux = Stream.of(complete.result())
|
||||
.filter(d -> d.getGroupId() == groupId)
|
||||
.toList();
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(aux));
|
||||
} else {
|
||||
context.fail(500, complete.cause());
|
||||
}
|
||||
});
|
||||
|
||||
this.restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_DEVICES, Device[].class, resultList);
|
||||
}
|
||||
|
||||
private void getDeviceSensors(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
Promise<Sensor[]> resultList = Promise.promise();
|
||||
resultList.future().onComplete(result -> {
|
||||
if (result.succeeded()) {
|
||||
Sensor[] sensors = result.result();
|
||||
List<Sensor> aux = Arrays.stream(sensors)
|
||||
.filter(s -> s.getDeviceId() == deviceId)
|
||||
.toList();
|
||||
context.response().putHeader("Content-Type", "application/json").end(gson.toJson(aux));
|
||||
} else {
|
||||
context.response().setStatusCode(500).end(result.cause().getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_SENSORS, Sensor[].class, resultList);
|
||||
}
|
||||
|
||||
private void getDeviceActuators(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
Promise<Actuator[]> resultList = Promise.promise();
|
||||
resultList.future().onComplete(result -> {
|
||||
if (result.succeeded()) {
|
||||
Actuator[] devices = result.result();
|
||||
List<Actuator> aux = Arrays.stream(devices)
|
||||
.filter(a -> a.getDeviceId() == deviceId)
|
||||
.toList();
|
||||
context.response().putHeader("Content-Type", "application/json").end(gson.toJson(aux));
|
||||
} else {
|
||||
context.response().setStatusCode(500).end(result.cause().getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_ACTUATORS, Actuator[].class, resultList);
|
||||
}
|
||||
|
||||
private void getDeviceLatestValues(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
|
||||
Promise<DeviceLatestValuesView[]> resultList = Promise.promise();
|
||||
resultList.future().onComplete(complete -> {
|
||||
if (complete.succeeded()) {
|
||||
List<DeviceLatestValuesView> aux = Stream.of(complete.result())
|
||||
.filter(elem -> elem.getDeviceId() == deviceId)
|
||||
.toList();
|
||||
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(aux));
|
||||
} else {
|
||||
context.fail(500, complete.cause());
|
||||
}
|
||||
});
|
||||
|
||||
this.restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_LATEST_VALUES_VIEW, DeviceLatestValuesView[].class, resultList);
|
||||
}
|
||||
|
||||
private void getDevicePollutionMap(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
|
||||
Promise<DevicePollutionMap[]> resultList = Promise.promise();
|
||||
|
||||
resultList.future().onComplete(complete -> {
|
||||
if (complete.succeeded()) {
|
||||
List<DevicePollutionMap> aux = Arrays.asList(complete.result()).stream()
|
||||
.filter(elem -> elem.getDeviceId() == deviceId)
|
||||
.toList();
|
||||
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(aux));
|
||||
} else {
|
||||
context.fail(500, complete.cause());
|
||||
}
|
||||
});
|
||||
|
||||
this.restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_POLLUTION_MAP_VIEW, DevicePollutionMap[].class, resultList);
|
||||
}
|
||||
|
||||
private void getDeviceHistory(RoutingContext context) {
|
||||
Integer deviceId = Integer.parseInt(context.request().getParam("deviceId"));
|
||||
|
||||
Promise<DeviceSensorHistory[]> resultList = Promise.promise();
|
||||
|
||||
resultList.future().onComplete(complete -> {
|
||||
if (complete.succeeded()) {
|
||||
List<DeviceSensorHistory> aux = Arrays.asList(complete.result()).stream()
|
||||
.filter(elem -> elem.getDeviceId() == deviceId)
|
||||
.toList();
|
||||
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(aux));
|
||||
} else {
|
||||
context.fail(500, complete.cause());
|
||||
}
|
||||
});
|
||||
|
||||
this.restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_SENSOR_HISTORY_BY_DEVICE_VIEW, DeviceSensorHistory[].class, resultList);
|
||||
}
|
||||
|
||||
private void getSensorValues(RoutingContext context) {
|
||||
Integer sensorId = Integer.parseInt(context.request().getParam("sensorId"));
|
||||
|
||||
Promise<DeviceSensorValue[]> resultList = Promise.promise();
|
||||
|
||||
resultList.future().onComplete(complete -> {
|
||||
if (complete.succeeded()) {
|
||||
List<DeviceSensorValue> aux = Arrays.asList(complete.result()).stream()
|
||||
.filter(val -> val.getSensorId() == sensorId)
|
||||
.toList();
|
||||
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(aux));
|
||||
} else {
|
||||
context.fail(500, complete.cause());
|
||||
}
|
||||
});
|
||||
|
||||
this.restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.GET_SENSOR_VALUES_VIEW, DeviceSensorValue[].class, resultList);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package net.miarma.contaminus.server;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
|
||||
public class MqttVerticle extends AbstractVerticle {
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.miarma.contaminus.server;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.handler.StaticHandler;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
|
||||
public class WebServerVerticle extends AbstractVerticle {
|
||||
private ConfigManager configManager;
|
||||
|
||||
public WebServerVerticle() {
|
||||
configManager = ConfigManager.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
Constants.LOGGER.info("📡 Iniciando WebServerVerticle...");
|
||||
|
||||
Router router = Router.router(vertx);
|
||||
|
||||
Path webRootPath = Paths.get(configManager.getWebRoot());
|
||||
|
||||
if (webRootPath.isAbsolute()) {
|
||||
Path basePath = Paths.get(System.getProperty("user.dir")); // Directorio actual
|
||||
webRootPath = basePath.relativize(webRootPath);
|
||||
}
|
||||
|
||||
router.route("/*")
|
||||
.handler(
|
||||
StaticHandler.create(webRootPath.toString())
|
||||
.setCachingEnabled(false)
|
||||
.setDefaultContentEncoding("UTF-8")
|
||||
);
|
||||
|
||||
router.route("/dashboard/*").handler(ctx -> {
|
||||
ctx.reroute("/index.html");
|
||||
});
|
||||
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getWebserverPort(), configManager.getHost());
|
||||
|
||||
startPromise.complete();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import java.util.Map;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
@@ -12,128 +12,46 @@ import io.vertx.ext.web.client.WebClient;
|
||||
|
||||
public class RestClientUtil {
|
||||
|
||||
public WebClient client;
|
||||
private Gson gson;
|
||||
private final WebClient client;
|
||||
private final Gson gson;
|
||||
|
||||
public RestClientUtil(WebClient client) {
|
||||
gson = new Gson();
|
||||
this.client = client;
|
||||
}
|
||||
public RestClientUtil(WebClient client) {
|
||||
this.client = client;
|
||||
this.gson = new Gson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request utility
|
||||
*
|
||||
* @param <T> Type of result enveloped in JSON response
|
||||
* @param port Port
|
||||
* @param host Host address
|
||||
* @param resource URI where resource is provided
|
||||
* @param classType Type of result enveloped in JSON response
|
||||
* @param promise Promise to be executed on call finish
|
||||
*/
|
||||
public <T> void getRequest(Integer port, String host, String resource, Class<T> classType, Promise<T> promise) {
|
||||
client.getAbs(host + ":" + port + resource).send(elem -> {
|
||||
if (elem.succeeded()) {
|
||||
promise.complete(gson.fromJson(elem.result().bodyAsString(), classType));
|
||||
} else {
|
||||
promise.fail(elem.cause());
|
||||
}
|
||||
});
|
||||
public <T> Future<T> getRequest(int port, String host, String resource, Class<T> classType) {
|
||||
return client.getAbs(host + ":" + port + resource)
|
||||
.send()
|
||||
.map(response -> gson.fromJson(response.bodyAsString(), classType));
|
||||
}
|
||||
|
||||
}
|
||||
public <T> Future<T> getRequestWithParams(int port, String host, String resource, Class<T> classType,
|
||||
Map<String, String> params) {
|
||||
HttpRequest<Buffer> httpRequest = client.getAbs(host + ":" + port + "/" + resource);
|
||||
params.forEach(httpRequest::addQueryParam);
|
||||
|
||||
/**
|
||||
* Get request utility
|
||||
*
|
||||
* @param <T> Type of result enveloped in JSON response
|
||||
* @param port Port
|
||||
* @param host Host address
|
||||
* @param resource URI where resource is provided
|
||||
* @param classType Type of result enveloped in JSON response
|
||||
* @param promise Promise to be executed on call finish
|
||||
* @param params Map with key-value entries for call parameters
|
||||
*/
|
||||
public <T> void getRequestWithParams(Integer port, String host, String resource, Class<T> classType,
|
||||
Promise<T> promise, Map<String, String> params) {
|
||||
HttpRequest<Buffer> httpRequest = client.getAbs(host + ":" + port + "/" + resource);
|
||||
return httpRequest.send()
|
||||
.map(response -> gson.fromJson(response.bodyAsString(), classType));
|
||||
}
|
||||
|
||||
params.forEach((key, value) -> {
|
||||
httpRequest.addQueryParam(key, value);
|
||||
});
|
||||
public <B, T> Future<T> postRequest(int port, String host, String resource, B body, Class<T> classType) {
|
||||
JsonObject jsonBody = new JsonObject(gson.toJson(body));
|
||||
return client.postAbs(host + ":" + port + "/" + resource)
|
||||
.sendJsonObject(jsonBody)
|
||||
.map(response -> gson.fromJson(response.bodyAsString(), classType));
|
||||
}
|
||||
|
||||
httpRequest.send(elem -> {
|
||||
if (elem.succeeded()) {
|
||||
promise.complete(gson.fromJson(elem.result().bodyAsString(), classType));
|
||||
} else {
|
||||
promise.fail(elem.cause());
|
||||
}
|
||||
});
|
||||
public <B, T> Future<T> putRequest(int port, String host, String resource, B body, Class<T> classType) {
|
||||
JsonObject jsonBody = new JsonObject(gson.toJson(body));
|
||||
return client.putAbs(host + ":" + port + "/" + resource)
|
||||
.sendJsonObject(jsonBody)
|
||||
.map(response -> gson.fromJson(response.bodyAsString(), classType));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Post request utility
|
||||
*
|
||||
* @param <B> Type of body enveloped in JSON request
|
||||
* @param <T> Type of result enveloped in JSON response
|
||||
* @param port Port
|
||||
* @param host Host address
|
||||
* @param resource URI where resource is provided
|
||||
* @param classType Type of result enveloped in JSON response
|
||||
* @param promise Promise to be executed on call finish
|
||||
*/
|
||||
public <B, T> void postRequest(Integer port, String host, String resource, Object body, Class<T> classType,
|
||||
Promise<T> promise) {
|
||||
JsonObject jsonBody = new JsonObject(gson.toJson(body));
|
||||
client.postAbs(host + ":" + port + "/" + resource).sendJsonObject(jsonBody, elem -> {
|
||||
if (elem.succeeded()) {
|
||||
Gson gson = new Gson();
|
||||
promise.complete(gson.fromJson(elem.result().bodyAsString(), classType));
|
||||
} else {
|
||||
promise.fail(elem.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Put request utility
|
||||
*
|
||||
* @param <B> Type of body enveloped in JSON request
|
||||
* @param <T> Type of result enveloped in JSON response
|
||||
* @param port Port
|
||||
* @param host Host address
|
||||
* @param resource URI where resource is provided
|
||||
* @param classType Type of result enveloped in JSON response
|
||||
* @param promise Promise to be executed on call finish
|
||||
*/
|
||||
public <B, T> void putRequest(Integer port, String host, String resource, Object body, Class<T> classType,
|
||||
Promise<T> promise) {
|
||||
JsonObject jsonBody = new JsonObject(gson.toJson(body));
|
||||
client.putAbs(host + ":" + port + "/" + resource).sendJsonObject(jsonBody, elem -> {
|
||||
if (elem.succeeded()) {
|
||||
Gson gson = new Gson();
|
||||
promise.complete(gson.fromJson(elem.result().bodyAsString(), classType));
|
||||
} else {
|
||||
promise.fail(elem.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete request utility
|
||||
*
|
||||
* @param port Port
|
||||
* @param host Host address
|
||||
* @param resource URI where resource is provided
|
||||
* @param promise Promise to be executed on call finish
|
||||
*/
|
||||
public void deleteRequest(Integer port, String host, String resource, Promise<String> promise) {
|
||||
client.deleteAbs(host + ":" + port + "/" + resource).send(elem -> {
|
||||
if (elem.succeeded()) {
|
||||
promise.complete(elem.result().bodyAsString());
|
||||
} else {
|
||||
promise.fail(elem.cause());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
public Future<String> deleteRequest(int port, String host, String resource) {
|
||||
return client.deleteAbs(host + ":" + port + "/" + resource)
|
||||
.send()
|
||||
.map(response -> response.bodyAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
package net.miarma.contaminus.verticles;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.http.HttpMethod;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import io.vertx.ext.web.handler.CorsHandler;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.common.SingleJsonResponse;
|
||||
import net.miarma.contaminus.dao.ActuatorDAO;
|
||||
import net.miarma.contaminus.dao.COValueDAO;
|
||||
import net.miarma.contaminus.dao.DeviceDAO;
|
||||
import net.miarma.contaminus.dao.GpsValueDAO;
|
||||
import net.miarma.contaminus.dao.GroupDAO;
|
||||
import net.miarma.contaminus.dao.SensorDAO;
|
||||
import net.miarma.contaminus.dao.WeatherValueDAO;
|
||||
import net.miarma.contaminus.dao.views.ViewLatestValuesDAO;
|
||||
import net.miarma.contaminus.dao.views.ViewPollutionMapDAO;
|
||||
import net.miarma.contaminus.dao.views.ViewSensorHistoryDAO;
|
||||
import net.miarma.contaminus.dao.views.ViewSensorValueDAO;
|
||||
import net.miarma.contaminus.db.DatabaseManager;
|
||||
import net.miarma.contaminus.db.DatabaseProvider;
|
||||
import net.miarma.contaminus.db.QueryBuilder;
|
||||
import net.miarma.contaminus.entities.Actuator;
|
||||
import net.miarma.contaminus.entities.COValue;
|
||||
import net.miarma.contaminus.entities.Device;
|
||||
import net.miarma.contaminus.entities.GpsValue;
|
||||
import net.miarma.contaminus.entities.Group;
|
||||
import net.miarma.contaminus.entities.Sensor;
|
||||
import net.miarma.contaminus.entities.ViewLatestValues;
|
||||
import net.miarma.contaminus.entities.ViewPollutionMap;
|
||||
import net.miarma.contaminus.entities.ViewSensorHistory;
|
||||
import net.miarma.contaminus.entities.ViewSensorValue;
|
||||
import net.miarma.contaminus.entities.WeatherValue;
|
||||
|
||||
|
||||
/*
|
||||
* This class is a Verticle that will handle the Data Layer API.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DataLayerAPIVerticle extends AbstractVerticle {
|
||||
private DatabaseManager dbManager;
|
||||
private ConfigManager configManager;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
private Pool pool;
|
||||
|
||||
private GroupDAO groupDAO;
|
||||
private DeviceDAO deviceDAO;
|
||||
private SensorDAO sensorDAO;
|
||||
private ActuatorDAO actuatorDAO;
|
||||
private COValueDAO coValueDAO;
|
||||
private WeatherValueDAO weatherValueDAO;
|
||||
private GpsValueDAO gpsValueDAO;
|
||||
private ViewLatestValuesDAO viewLatestValuesDAO;
|
||||
private ViewPollutionMapDAO viewPollutionMapDAO;
|
||||
private ViewSensorHistoryDAO viewSensorHistoryDAO;
|
||||
private ViewSensorValueDAO viewSensorValueDAO;
|
||||
|
||||
public DataLayerAPIVerticle() {
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
Constants.LOGGER.info("📡 Iniciando DataLayerAPIVerticle...");
|
||||
|
||||
this.pool = DatabaseProvider.createPool(vertx, configManager);
|
||||
this.dbManager = DatabaseManager.getInstance(pool);
|
||||
|
||||
this.groupDAO = new GroupDAO(pool);
|
||||
this.deviceDAO = new DeviceDAO(pool);
|
||||
this.sensorDAO = new SensorDAO(pool);
|
||||
this.actuatorDAO = new ActuatorDAO(pool);
|
||||
this.coValueDAO = new COValueDAO(pool);
|
||||
this.weatherValueDAO = new WeatherValueDAO(pool);
|
||||
this.gpsValueDAO = new GpsValueDAO(pool);
|
||||
this.viewLatestValuesDAO = new ViewLatestValuesDAO(pool);
|
||||
this.viewPollutionMapDAO = new ViewPollutionMapDAO(pool);
|
||||
this.viewSensorHistoryDAO = new ViewSensorHistoryDAO(pool);
|
||||
this.viewSensorValueDAO = new ViewSensorValueDAO(pool);
|
||||
|
||||
Router router = Router.router(vertx);
|
||||
Set<HttpMethod> allowedMethods = new HashSet<>(
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.OPTIONS)); // Por ejemplo
|
||||
Set<String> allowedHeaders = new HashSet<>(Arrays.asList("Content-Type", "Authorization"));
|
||||
|
||||
router.route().handler(CorsHandler.create()
|
||||
.allowCredentials(true)
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowedMethods(allowedMethods));
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
// Group Routes
|
||||
router.route(HttpMethod.GET, Constants.GROUPS).handler(this::getAllGroups);
|
||||
router.route(HttpMethod.GET, Constants.GROUP).handler(this::getGroupById);
|
||||
router.route(HttpMethod.POST, Constants.GROUPS).handler(this::addGroup);
|
||||
router.route(HttpMethod.PUT, Constants.GROUP).handler(this::updateGroup);
|
||||
|
||||
// Device Routes
|
||||
router.route(HttpMethod.GET, Constants.DEVICES).handler(this::getAllDevices);
|
||||
router.route(HttpMethod.GET, Constants.DEVICE).handler(this::getDeviceById);
|
||||
router.route(HttpMethod.POST, Constants.DEVICES).handler(this::addDevice);
|
||||
router.route(HttpMethod.PUT, Constants.DEVICE).handler(this::updateDevice);
|
||||
router.route(HttpMethod.GET, Constants.DEVICE_GROUP_ID).handler(this::getDeviceGroupId);
|
||||
|
||||
// Sensor Routes
|
||||
router.route(HttpMethod.GET, Constants.SENSORS).handler(this::getAllSensors);
|
||||
router.route(HttpMethod.GET, Constants.SENSOR).handler(this::getSensorById);
|
||||
router.route(HttpMethod.POST, Constants.SENSORS).handler(this::addSensor);
|
||||
router.route(HttpMethod.PUT, Constants.SENSOR).handler(this::updateSensor);
|
||||
router.route(HttpMethod.POST, Constants.ADD_GPS_VALUE).handler(this::addGpsValue);
|
||||
router.route(HttpMethod.POST, Constants.ADD_WEATHER_VALUE).handler(this::addWeatherValue);
|
||||
router.route(HttpMethod.POST, Constants.ADD_CO_VALUE).handler(this::addCoValue);
|
||||
|
||||
|
||||
// Actuator Routes
|
||||
router.route(HttpMethod.GET, Constants.ACTUATORS).handler(this::getAllActuators);
|
||||
router.route(HttpMethod.GET, Constants.ACTUATOR).handler(this::getActuatorById);
|
||||
router.route(HttpMethod.POST, Constants.ACTUATORS).handler(this::addActuator);
|
||||
router.route(HttpMethod.PUT, Constants.ACTUATOR).handler(this::updateActuator);
|
||||
|
||||
// Views Routes
|
||||
router.route(HttpMethod.GET, Constants.VIEW_LATEST_VALUES).handler(this::getLatestValuesView);
|
||||
router.route(HttpMethod.GET, Constants.VIEW_POLLUTION_MAP).handler(this::getDevicePollutionMapView);
|
||||
router.route(HttpMethod.GET, Constants.VIEW_SENSOR_VALUES).handler(this::getSensorValuesView);
|
||||
router.route(HttpMethod.GET, Constants.VIEW_SENSOR_HISTORY).handler(this::getSensorHistoryByDeviceView);
|
||||
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getDataApiPort(), configManager.getHost());
|
||||
|
||||
pool.query("SELECT 1").execute(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 Connected to DB");
|
||||
startPromise.complete();
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 Failed to connect to DB: " + ar.cause());
|
||||
startPromise.fail(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllGroups(RoutingContext context) {
|
||||
groupDAO.getAll()
|
||||
.onSuccess(groups -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(groups));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getGroupById(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
|
||||
groupDAO.getById(groupId)
|
||||
.onSuccess(group -> {
|
||||
if (group != null) {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(group));
|
||||
} else {
|
||||
context.response().setStatusCode(404).end();
|
||||
}
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void addGroup(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Group group = gson.fromJson(body.toString(), Group.class);
|
||||
|
||||
groupDAO.insert(group)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Group.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGroup(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Group group = gson.fromJson(body.toString(), Group.class);
|
||||
|
||||
groupDAO.update(group)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Group.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllDevices(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
|
||||
deviceDAO.getAllByGroupId(groupId)
|
||||
.onSuccess(devices -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(devices));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getDeviceById(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
|
||||
deviceDAO.getByIdAndGroupId(deviceId, groupId)
|
||||
.onSuccess(device -> {
|
||||
if (device != null) {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(device));
|
||||
} else {
|
||||
context.response().setStatusCode(404).end();
|
||||
}
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getDeviceGroupId(RoutingContext context) {
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
|
||||
deviceDAO.getById(deviceId)
|
||||
.onSuccess(device -> {
|
||||
if (device != null) {
|
||||
Integer groupId = device.getGroupId();
|
||||
SingleJsonResponse<Integer> response = SingleJsonResponse.of(groupId);
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(response));
|
||||
} else {
|
||||
context.response().setStatusCode(404).end("Device not found");
|
||||
}
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void addDevice(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Device device = gson.fromJson(body.toString(), Device.class);
|
||||
|
||||
deviceDAO.insert(device)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Device.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDevice(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Device device = gson.fromJson(body.toString(), Device.class);
|
||||
|
||||
deviceDAO.update(device)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Device.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllSensors(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
|
||||
deviceDAO.getByIdAndGroupId(deviceId, groupId).compose(device -> {
|
||||
if (device == null) {
|
||||
return Future.succeededFuture(List.of());
|
||||
}
|
||||
return sensorDAO.getAllByDeviceId(device.getDeviceId());
|
||||
}).onSuccess(sensors -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(sensors));
|
||||
}).onFailure(err -> {
|
||||
context.response().setStatusCode(500).end("Error: " + err.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorById(RoutingContext context) {
|
||||
Integer sensorId = Integer.parseInt(context.request().getParam("sensorId"));
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
|
||||
deviceDAO.getByIdAndGroupId(deviceId, groupId).compose(device -> {
|
||||
if (device == null) {
|
||||
return Future.succeededFuture(null);
|
||||
}
|
||||
return sensorDAO.getByIdAndDeviceId(sensorId, device.getDeviceId());
|
||||
}).onSuccess(sensor -> {
|
||||
if (sensor == null) {
|
||||
context.response().setStatusCode(404).end("Sensor no encontrado");
|
||||
return;
|
||||
}
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(sensor));
|
||||
}).onFailure(err -> {
|
||||
context.response().setStatusCode(500).end("Error: " + err.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private void addSensor(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Sensor sensor = gson.fromJson(body.toString(), Sensor.class);
|
||||
|
||||
sensorDAO.insert(sensor)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Sensor.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSensor(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Sensor sensor = gson.fromJson(body.toString(), Sensor.class);
|
||||
|
||||
sensorDAO.update(sensor)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Sensor.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void addGpsValue(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
GpsValue gpsValue = gson.fromJson(body.toString(), GpsValue.class);
|
||||
|
||||
gpsValueDAO.insert(gpsValue)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.setStatusCode(201)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(gson.toJson(result, GpsValue.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void addWeatherValue(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
WeatherValue weatherValue = gson.fromJson(body.toString(), WeatherValue.class);
|
||||
|
||||
weatherValueDAO.insert(weatherValue)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.setStatusCode(201)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(gson.toJson(result, WeatherValue.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void addCoValue(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
COValue coValue = gson.fromJson(body.toString(), COValue.class);
|
||||
|
||||
coValueDAO.insert(coValue)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.setStatusCode(201)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(gson.toJson(result, COValue.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getAllActuators(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
|
||||
deviceDAO.getByIdAndGroupId(deviceId, groupId).compose(device -> {
|
||||
if (device == null) {
|
||||
return Future.succeededFuture(List.of());
|
||||
}
|
||||
return actuatorDAO.getAllByDeviceId(device.getDeviceId());
|
||||
}).onSuccess(actuators -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(actuators));
|
||||
}).onFailure(err -> {
|
||||
context.response().setStatusCode(500).end("Error: " + err.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private void getActuatorById(RoutingContext context) {
|
||||
Integer groupId = Integer.parseInt(context.request().getParam("groupId"));
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
Integer actuatorId = Integer.parseInt(context.request().getParam("actuatorId"));
|
||||
|
||||
deviceDAO.getByIdAndGroupId(deviceId, groupId).compose(device -> {
|
||||
if (device == null) {
|
||||
return Future.failedFuture(new RuntimeException("Dispositivo no encontrado"));
|
||||
}
|
||||
return actuatorDAO.getByIdAndDeviceId(actuatorId, device.getDeviceId());
|
||||
}).onSuccess(actuator -> {
|
||||
if (actuator == null) {
|
||||
context.response().setStatusCode(404).end("Actuator no encontrado");
|
||||
return;
|
||||
}
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(actuator));
|
||||
}).onFailure(err -> {
|
||||
context.response().setStatusCode(500).end("Error: " + err.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private void addActuator(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Actuator actuator = gson.fromJson(body.toString(), Actuator.class);
|
||||
|
||||
actuatorDAO.insert(actuator)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Actuator.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateActuator(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
Actuator actuator = gson.fromJson(body.toString(), Actuator.class);
|
||||
|
||||
actuatorDAO.update(actuator)
|
||||
.onSuccess(result -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(result, Actuator.class));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
context.fail(500, err);
|
||||
});
|
||||
}
|
||||
|
||||
private void getLatestValuesView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(ViewLatestValues.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, ViewLatestValues.class,
|
||||
onSuccess -> {
|
||||
Constants.LOGGER.info(gson.toJson(onSuccess));
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getDevicePollutionMapView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(ViewPollutionMap.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, ViewPollutionMap.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorValuesView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(ViewSensorValue.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, ViewSensorValue.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
private void getSensorHistoryByDeviceView(RoutingContext context) {
|
||||
String query = QueryBuilder
|
||||
.select(ViewSensorHistory.class)
|
||||
.build();
|
||||
|
||||
dbManager.execute(query, ViewSensorHistory.class,
|
||||
onSuccess -> {
|
||||
context.response()
|
||||
.putHeader("content-type", "application/json; charset=utf-8")
|
||||
.end(gson.toJson(onSuccess));
|
||||
},
|
||||
onFailure -> {
|
||||
context.fail(500, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package net.miarma.contaminus.verticles;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.HttpMethod;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientOptions;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
import io.vertx.ext.web.handler.CorsHandler;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import net.miarma.contaminus.common.ConfigManager;
|
||||
import net.miarma.contaminus.common.Constants;
|
||||
import net.miarma.contaminus.common.VoronoiZoneDetector;
|
||||
import net.miarma.contaminus.entities.Actuator;
|
||||
import net.miarma.contaminus.entities.COValue;
|
||||
import net.miarma.contaminus.entities.Device;
|
||||
import net.miarma.contaminus.entities.GpsValue;
|
||||
import net.miarma.contaminus.entities.ViewLatestValues;
|
||||
import net.miarma.contaminus.entities.ViewPollutionMap;
|
||||
import net.miarma.contaminus.entities.ViewSensorHistory;
|
||||
import net.miarma.contaminus.entities.ViewSensorValue;
|
||||
import net.miarma.contaminus.entities.WeatherValue;
|
||||
import net.miarma.contaminus.util.RestClientUtil;
|
||||
|
||||
public class LogicLayerAPIVerticle extends AbstractVerticle {
|
||||
private ConfigManager configManager;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
private RestClientUtil restClient;
|
||||
private MqttClient mqttClient;
|
||||
private VoronoiZoneDetector detector;
|
||||
|
||||
public LogicLayerAPIVerticle() {
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
WebClientOptions options = new WebClientOptions()
|
||||
.setUserAgent("ContaminUS");
|
||||
this.restClient = new RestClientUtil(WebClient.create(Vertx.vertx(), options));
|
||||
this.mqttClient = MqttClient.create(Vertx.vertx(),
|
||||
new MqttClientOptions()
|
||||
.setAutoKeepAlive(true)
|
||||
.setUsername("contaminus")
|
||||
.setPassword("contaminus")
|
||||
);
|
||||
this.detector = VoronoiZoneDetector.create("https://miarma.net/files/voronoi_sevilla_geovoronoi.geojson", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
Constants.LOGGER.info("📡 Iniciando LogicApiVerticle...");
|
||||
|
||||
Router router = Router.router(vertx);
|
||||
Set<HttpMethod> allowedMethods = new HashSet<>(
|
||||
Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.OPTIONS));
|
||||
Set<String> allowedHeaders = new HashSet<>(Arrays.asList("Content-Type", "Authorization"));
|
||||
|
||||
router.route().handler(CorsHandler.create()
|
||||
.allowCredentials(true)
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowedMethods(allowedMethods));
|
||||
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
router.route(HttpMethod.POST, Constants.BATCH).handler(this::addBatch);
|
||||
router.route(HttpMethod.GET, Constants.LATEST_VALUES).handler(this::getDeviceLatestValues);
|
||||
router.route(HttpMethod.GET, Constants.POLLUTION_MAP).handler(this::getDevicePollutionMap);
|
||||
router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory);
|
||||
router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues);
|
||||
router.route(HttpMethod.GET, Constants.ACTUATOR_STATUS).handler(this::getActuatorStatus);
|
||||
router.route(HttpMethod.POST, Constants.ACTUATOR_STATUS).handler(this::postActuatorStatus);
|
||||
|
||||
mqttClient.connect(1883, "localhost", ar -> {
|
||||
if (ar.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 MQTT client connected");
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getLogicApiPort(), configManager.getHost(), http -> {
|
||||
if (http.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 HTTP server started on port " + configManager.getLogicApiPort());
|
||||
startPromise.complete();
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 HTTP server failed to start: " + http.cause());
|
||||
startPromise.fail(http.cause());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 MQTT client connection failed: " + ar.cause());
|
||||
startPromise.fail(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getDeviceLatestValues(RoutingContext context) {
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.VIEW_LATEST_VALUES, ViewLatestValues[].class)
|
||||
.onSuccess(result -> {
|
||||
List<ViewLatestValues> aux = Arrays.stream(result)
|
||||
.filter(elem -> deviceId.equals(elem.getDeviceId()))
|
||||
.toList();
|
||||
context.response().putHeader("content-type", "application/json; charset=utf-8").end(gson.toJson(aux));
|
||||
})
|
||||
.onFailure(err -> context.fail(500, err));
|
||||
}
|
||||
|
||||
private void getDevicePollutionMap(RoutingContext context) {
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.VIEW_POLLUTION_MAP, ViewPollutionMap[].class)
|
||||
.onSuccess(result -> {
|
||||
List<ViewPollutionMap> aux = Arrays.asList(result).stream()
|
||||
.filter(elem -> deviceId.equals(elem.getDeviceId()))
|
||||
.toList();
|
||||
context.response().putHeader("content-type", "application/json; charset=utf-8").end(gson.toJson(aux));
|
||||
})
|
||||
.onFailure(err -> context.fail(500, err));
|
||||
}
|
||||
|
||||
private void getDeviceHistory(RoutingContext context) {
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.VIEW_SENSOR_HISTORY, ViewSensorHistory[].class)
|
||||
.onSuccess(result -> {
|
||||
List<ViewSensorHistory> aux = Arrays.asList(result).stream()
|
||||
.filter(elem -> deviceId.equals(elem.getDeviceId()))
|
||||
.toList();
|
||||
context.response().putHeader("content-type", "application/json; charset=utf-8").end(gson.toJson(aux));
|
||||
})
|
||||
.onFailure(err -> context.fail(500, err));
|
||||
}
|
||||
|
||||
private void getSensorValues(RoutingContext context) {
|
||||
int sensorId = Integer.parseInt(context.request().getParam("sensorId"));
|
||||
restClient.getRequest(configManager.getDataApiPort(), "http://" + configManager.getHost(),
|
||||
Constants.VIEW_SENSOR_VALUES, ViewSensorValue[].class)
|
||||
.onSuccess(result -> {
|
||||
List<ViewSensorValue> aux = Arrays.asList(result).stream()
|
||||
.filter(val -> val.getSensorId() == sensorId)
|
||||
.toList();
|
||||
context.response().putHeader("content-type", "application/json; charset=utf-8").end(gson.toJson(aux));
|
||||
})
|
||||
.onFailure(err -> context.fail(500, err));
|
||||
}
|
||||
|
||||
private void addBatch(RoutingContext context) {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
String groupId = body.getString("groupId");
|
||||
String deviceId = body.getString("deviceId");
|
||||
|
||||
JsonObject gpsJson = body.getJsonObject("gps");
|
||||
JsonObject weatherJson = body.getJsonObject("weather");
|
||||
JsonObject coJson = body.getJsonObject("co");
|
||||
|
||||
if (groupId == null || deviceId == null || gpsJson == null || weatherJson == null || coJson == null) {
|
||||
sendError(context, 400, "Missing required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
GpsValue gpsValue = gson.fromJson(gpsJson.toString(), GpsValue.class);
|
||||
WeatherValue weatherValue = gson.fromJson(weatherJson.toString(), WeatherValue.class);
|
||||
COValue coValue = gson.fromJson(coJson.toString(), COValue.class);
|
||||
|
||||
if (!isInCorrectZone(gpsValue, groupId)) {
|
||||
sendZoneWarning(context);
|
||||
return;
|
||||
}
|
||||
|
||||
handleActuators(groupId, coValue.getValue());
|
||||
|
||||
gpsValue.setDeviceId(deviceId);
|
||||
weatherValue.setDeviceId(deviceId);
|
||||
coValue.setDeviceId(deviceId);
|
||||
|
||||
storeMeasurements(context, groupId, deviceId, gpsValue, weatherValue, coValue);
|
||||
}
|
||||
|
||||
private void getActuatorStatus(RoutingContext context) {
|
||||
String groupId = context.request().getParam("groupId");
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
String actuatorId = context.request().getParam("actuatorId");
|
||||
|
||||
String host = "http://" + configManager.getHost();
|
||||
int port = configManager.getDataApiPort();
|
||||
String actuatorPath = Constants.ACTUATOR
|
||||
.replace(":groupId", groupId)
|
||||
.replace(":deviceId", deviceId)
|
||||
.replace(":actuatorId", actuatorId);
|
||||
|
||||
restClient.getRequest(port, host, actuatorPath, Actuator.class)
|
||||
.onSuccess(actuator -> {
|
||||
String actuatorStatus = actuator.getStatus() == 0 ? "Solo vehiculos electricos/hibridos" : "Todo tipo de vehiculos";
|
||||
|
||||
context.response()
|
||||
.setStatusCode(200)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(new JsonObject().put("status", "success").put("actuatorStatus", actuatorStatus).encode());
|
||||
})
|
||||
.onFailure(_ -> sendError(context, 500, "Failed to retrieve actuator status"));
|
||||
}
|
||||
|
||||
private void postActuatorStatus(RoutingContext context) {
|
||||
String groupId = context.request().getParam("groupId");
|
||||
String deviceId = context.request().getParam("deviceId");
|
||||
String actuatorId = context.request().getParam("actuatorId");
|
||||
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
String actuatorStatus = body.getString("status");
|
||||
|
||||
String host = "http://" + configManager.getHost();
|
||||
int port = configManager.getDataApiPort();
|
||||
String actuatorPath = Constants.ACTUATOR
|
||||
.replace(":groupId", groupId)
|
||||
.replace(":deviceId", deviceId)
|
||||
.replace(":actuatorId", actuatorId);
|
||||
|
||||
Actuator updatedActuator = new Actuator(null, null, Integer.valueOf(actuatorStatus), null); // Assuming status 1 is the desired state
|
||||
|
||||
restClient.putRequest(port, host, actuatorPath, updatedActuator, Actuator.class)
|
||||
.onSuccess(_ -> {
|
||||
context.response()
|
||||
.setStatusCode(200)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(new JsonObject().put("status", "success").put("message", "Actuator status updated").encode());
|
||||
})
|
||||
.onFailure(_ -> sendError(context, 500, "Failed to update actuator status"));
|
||||
}
|
||||
|
||||
private void sendError(RoutingContext ctx, int status, String msg) {
|
||||
ctx.response().setStatusCode(status).end(msg);
|
||||
}
|
||||
|
||||
private boolean isInCorrectZone(GpsValue gps, String expectedZone) {
|
||||
Integer actualZone = detector.getZoneForPoint(gps.getLon(), gps.getLat());
|
||||
Constants.LOGGER.info(gps.getLat() + ", " + gps.getLon() + " -> Zone: " + actualZone);
|
||||
return actualZone.equals(Integer.valueOf(expectedZone));
|
||||
}
|
||||
|
||||
private void sendZoneWarning(RoutingContext ctx) {
|
||||
Constants.LOGGER.info("El dispositivo no ha medido en su zona");
|
||||
ctx.response()
|
||||
.setStatusCode(200)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(new JsonObject().put("status", "success").put("message", "Device did not measure in its zone").encode());
|
||||
}
|
||||
|
||||
private void handleActuators(String groupId, float coAmount) {
|
||||
String host = "http://" + configManager.getHost();
|
||||
int port = configManager.getDataApiPort();
|
||||
String devicesPath = Constants.DEVICES.replace(":groupId", groupId);
|
||||
|
||||
restClient.getRequest(port, host, devicesPath, Device[].class)
|
||||
.onSuccess(devices -> Arrays.stream(devices)
|
||||
.filter(d -> Constants.ACTUATOR_ROLE.equals(d.getDeviceRole()))
|
||||
.forEach(d -> {
|
||||
String topic = buildTopic(Integer.parseInt(groupId), d.getDeviceId(), "matrix");
|
||||
publishMQTT(topic, coAmount);
|
||||
|
||||
String actuatorsPath = Constants.ACTUATORS
|
||||
.replace(":groupId", groupId)
|
||||
.replace(":deviceId", d.getDeviceId());
|
||||
|
||||
restClient.getRequest(port, host, actuatorsPath, Actuator[].class)
|
||||
.onSuccess(actuators -> Arrays.stream(actuators).forEach(a -> {
|
||||
String actuatorPath = Constants.ACTUATOR
|
||||
.replace(":groupId", groupId)
|
||||
.replace(":deviceId", d.getDeviceId())
|
||||
.replace(":actuatorId", String.valueOf(a.getActuatorId()));
|
||||
Actuator updated = new Actuator(a.getActuatorId(), d.getDeviceId(), coAmount >= 80.0f ? 0 : 1, null);
|
||||
restClient.putRequest(port, host, actuatorPath, updated, Actuator.class);
|
||||
}))
|
||||
.onFailure(err -> Constants.LOGGER.error("Failed to update actuator", err));
|
||||
}))
|
||||
.onFailure(err -> Constants.LOGGER.error("Failed to retrieve devices", err));
|
||||
}
|
||||
|
||||
private void publishMQTT(String topic, float coAmount) {
|
||||
if (mqttClient.isConnected()) {
|
||||
Constants.LOGGER.info("Publishing to MQTT topic: " + topic);
|
||||
mqttClient.publish(topic, Buffer.buffer(coAmount >= 80.0f ? "ECO" : "GAS"),
|
||||
MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMeasurements(RoutingContext ctx, String groupId, String deviceId,
|
||||
GpsValue gps, WeatherValue weather, COValue co) {
|
||||
|
||||
String host = "http://" + configManager.getHost();
|
||||
int port = configManager.getDataApiPort();
|
||||
|
||||
String gpsPath = Constants.ADD_GPS_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId);
|
||||
String weatherPath = Constants.ADD_WEATHER_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId);
|
||||
String coPath = Constants.ADD_CO_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId);
|
||||
|
||||
restClient.postRequest(port, host, gpsPath, gps, GpsValue.class)
|
||||
.compose(_ -> restClient.postRequest(port, host, weatherPath, weather, WeatherValue.class))
|
||||
.compose(_ -> restClient.postRequest(port, host, coPath, co, COValue.class))
|
||||
.onSuccess(_ -> ctx.response()
|
||||
.setStatusCode(201)
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(new JsonObject().put("status", "success").put("inserted", 3).encode()))
|
||||
.onFailure(err -> ctx.fail(500, err));
|
||||
}
|
||||
|
||||
|
||||
private String buildTopic(int groupId, String deviceId, String topic) {
|
||||
String topicString = "group/" + groupId + "/device/" + deviceId + "/" + topic;
|
||||
return topicString;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.miarma.contaminus.server;
|
||||
package net.miarma.contaminus.verticles;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -52,7 +52,6 @@ public class MainVerticle extends AbstractVerticle {
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
try {
|
||||
System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
|
||||
init();
|
||||
deployVerticles(startPromise);
|
||||
} catch (Exception e) {
|
||||
@@ -85,16 +84,6 @@ public class MainVerticle extends AbstractVerticle {
|
||||
}
|
||||
});
|
||||
|
||||
vertx.deployVerticle(new WebServerVerticle(), result -> {
|
||||
if (result.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 WebServerVerticle desplegado");
|
||||
Constants.LOGGER.info("\t🔗 WEB SERVER URL: " + configManager.getHost()
|
||||
+ ":" + configManager.getWebserverPort());
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 Error deploying WebServerVerticle: " + result.cause());
|
||||
}
|
||||
});
|
||||
|
||||
startPromise.complete();
|
||||
}
|
||||
|
||||
18
backend/src/main/resources/coords.txt
Normal file
18
backend/src/main/resources/coords.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
Bellavista: 37.32533897043053, -5.968045749476732
|
||||
Bermejales: 37.3490769843888, -5.976422439823011
|
||||
Los Remedios: 37.37515235961667, -6.000317870442392
|
||||
Tiro de Línea: 37.36841756821152, -5.97598991091761
|
||||
San Bernardo: 37.3789653804891, -5.987731412877974
|
||||
Nervión: 37.38310913923199, -5.97320443871298
|
||||
Cerro del Águila: 37.3733690603403, -5.960390435428303
|
||||
Polígono Sur: 37.36134485686387, -5.965857306623599
|
||||
Amate: 37.381087013389035, -5.953545655643191
|
||||
Sevilla Este: 37.39944168632523, -5.925092000775878
|
||||
Valdezorras: 37.429014427259844, -5.927831833065477
|
||||
Pino Montano: 37.423390190328654, -5.973241522877093
|
||||
Macarena: 37.40735604254458, -5.980276144202576
|
||||
Centro: 37.39277256619283, -5.994765524658572
|
||||
Santa Justa: 37.394092596421686, -5.962662563452358
|
||||
Santa Clara: 37.40050556650672, -5.954710495719027
|
||||
Triana: 37.380789093980844, -6.008393542942082
|
||||
Cartuja: 37.403427709335816, -6.007627600183472
|
||||
@@ -4,11 +4,12 @@ db.host=localhost
|
||||
db.port=3306
|
||||
db.name=dad
|
||||
db.user=root
|
||||
db.pwd=root
|
||||
db.password=root
|
||||
dp.poolSize=5
|
||||
|
||||
# HTTP Server Configuration
|
||||
inet.host=localhost
|
||||
mqtt.host=localhost
|
||||
webserver.port=8080
|
||||
data-api.port=8081
|
||||
logic-api.port=8082
|
||||
20
backend/src/main/resources/logback.xml
Normal file
20
backend/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>
|
||||
%cyan([%d{HH:mm:ss}]) %highlight(%-5level) %green(%logger{20}) - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
||||
<logger name="io.netty" level="WARN"/>
|
||||
<logger name="io.vertx" level="INFO"/>
|
||||
<logger name="io.vertx.core.impl.launcher" level="INFO"/>
|
||||
<logger name="io.vertx.core.logging" level="INFO"/>
|
||||
|
||||
</configuration>
|
||||
325
backend/src/main/resources/openapi.yml
Normal file
325
backend/src/main/resources/openapi.yml
Normal file
@@ -0,0 +1,325 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: ContaminUS API
|
||||
version: 1.0.0
|
||||
description: Documentación de la API del proyecto ContaminUS
|
||||
servers:
|
||||
- url: http://localhost:8888
|
||||
description: Servidor local de desarrollo
|
||||
paths:
|
||||
/api/v1/groups/{groupId}/devices/{deviceId}/latest-values:
|
||||
get:
|
||||
summary: Últimos valores de los sensores del dispositivo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/v1/groups/{groupId}/devices/{deviceId}/pollution-map:
|
||||
get:
|
||||
summary: Mapa de contaminación del dispositivo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/v1/groups/{groupId}/devices/{deviceId}/history:
|
||||
get:
|
||||
summary: Historial de valores del dispositivo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/v1/groups/{groupId}/devices/{deviceId}/sensors/{sensorId}/values:
|
||||
get:
|
||||
summary: Valores del sensor
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sensorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups:
|
||||
get:
|
||||
summary: Lista de grupos
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}:
|
||||
get:
|
||||
summary: Información de un grupo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices:
|
||||
get:
|
||||
summary: Lista de dispositivos de un grupo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}:
|
||||
get:
|
||||
summary: Información de un dispositivo
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/sensors:
|
||||
get:
|
||||
summary: Lista de sensores
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/sensors/{sensorId}:
|
||||
get:
|
||||
summary: Información de un sensor
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sensorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/actuators:
|
||||
get:
|
||||
summary: Lista de actuadores
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuatorId}:
|
||||
get:
|
||||
summary: Información de un actuador
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: actuatorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuatorId}/status:
|
||||
get:
|
||||
summary: Estado de un actuador
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: actuatorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/v_latest_values:
|
||||
get:
|
||||
summary: Vista de últimos valores
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/v_pollution_map:
|
||||
get:
|
||||
summary: Vista de mapa de contaminación
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/v_sensor_history_by_device:
|
||||
get:
|
||||
summary: Vista de historial de sensores
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/v_sensor_values:
|
||||
get:
|
||||
summary: Vista de valores de sensores
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/v1/batch:
|
||||
post:
|
||||
summary: Insertar batch de datos
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/sensors/{sensorId}/gps_values:
|
||||
post:
|
||||
summary: Insertar valor GPS
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sensorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/sensors/{sensorId}/weather_values:
|
||||
post:
|
||||
summary: Insertar valor climático
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sensorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
/api/raw/v1/groups/{groupId}/devices/{deviceId}/sensors/{sensorId}/co_values:
|
||||
post:
|
||||
summary: Insertar valor de CO
|
||||
parameters:
|
||||
- name: groupId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: deviceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sensorId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Operación exitosa
|
||||
70
backend/src/main/resources/voronoi.py
Normal file
70
backend/src/main/resources/voronoi.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import geopandas as gpd # esto es necesario para leer el GeoJSON
|
||||
import matplotlib.pyplot as plt # esto es necesario para graficar
|
||||
import numpy as np # esto es necesario para manejar los arrays
|
||||
import json # esto es necesario para guardar el GeoJSON
|
||||
from shapely.geometry import Polygon # esto es necesario para crear la caja de recorte
|
||||
from geovoronoi import voronoi_regions_from_coords # esto es necesario para crear el Voronoi
|
||||
|
||||
# se cargan los puntos (coordenadas) de los actuadores
|
||||
# en formato GeoJSON
|
||||
points_gdf = gpd.read_file("sevilla.geojson")
|
||||
coords = np.array([[geom.x, geom.y] for geom in points_gdf.geometry])
|
||||
|
||||
# esto es una "caja" alrededor de Sevilla para recortar el Voronoi
|
||||
# para que las regiones no acotadas no sean infinitas
|
||||
seville_boundary = Polygon([
|
||||
(-6.10, 37.30),
|
||||
(-5.85, 37.30),
|
||||
(-5.85, 37.45),
|
||||
(-6.10, 37.45)
|
||||
])
|
||||
area_gdf = gpd.GeoDataFrame(geometry=[seville_boundary], crs="EPSG:4326")
|
||||
|
||||
# se genera el Voronoi con las coordenadas de los actuadores
|
||||
# y la caja de recorte (unión de todo)
|
||||
region_polys, region_pts = voronoi_regions_from_coords(coords, area_gdf.union_all())
|
||||
|
||||
# dibuja con matplotlib
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
||||
for poly in region_polys.values():
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.3, edgecolor='black')
|
||||
|
||||
ax.plot(coords[:, 0], coords[:, 1], 'ro')
|
||||
for idx, coord in enumerate(coords):
|
||||
ax.text(coord[0], coord[1], points_gdf.iloc[idx]["name"], fontsize=8, ha='center')
|
||||
|
||||
ax.set_title("Zonas Voronoi por Actuator (GeoVoronoi)")
|
||||
plt.axis("equal")
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Guardar GeoJSON
|
||||
features = []
|
||||
for idx, region in region_polys.items():
|
||||
point_index = region_pts[idx] # índice del punto original
|
||||
name = points_gdf.loc[point_index, "name"]
|
||||
if isinstance(name, gpd.GeoSeries):
|
||||
name = name.iloc[0] # o el que tú quieras
|
||||
name = str(name)
|
||||
|
||||
feature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"actuatorId": name
|
||||
},
|
||||
"geometry": json.loads(gpd.GeoSeries([region]).to_json())["features"][0]["geometry"]
|
||||
}
|
||||
features.append(feature)
|
||||
|
||||
geojson_output = {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
}
|
||||
|
||||
with open("voronoi_sevilla_geovoronoi.geojson", "w") as f:
|
||||
json.dump(geojson_output, f, indent=2)
|
||||
|
||||
print("✅ GeoJSON guardado como 'voronoi_sevilla_geovoronoi.geojson'")
|
||||
9
frontend/jsconfig.json
Normal file
9
frontend/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
2384
frontend/package-lock.json
generated
2384
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,19 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"axios": "^1.9.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.8",
|
||||
"framer-motion": "^12.14.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-router-dom": "^7.3.0"
|
||||
"react-router-dom": "^7.3.0",
|
||||
"swagger-ui-react": "^5.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
|
||||
1
frontend/public/.well-known/discord
Normal file
1
frontend/public/.well-known/discord
Normal file
@@ -0,0 +1 @@
|
||||
dh=39376f6548b4449fc0faf969d98f6f7a10af9e7e
|
||||
481
frontend/public/apidoc.json
Normal file
481
frontend/public/apidoc.json
Normal file
@@ -0,0 +1,481 @@
|
||||
{
|
||||
"name": "ContaminUS",
|
||||
"version": "1.0.0",
|
||||
"logic_api": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/batch",
|
||||
"description": "Añadir los valores de los sensores (batch)",
|
||||
"params": [
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "lat",
|
||||
"type": "float",
|
||||
"description": "Latitud",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "lon",
|
||||
"type": "float",
|
||||
"description": "Longitud",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "temperature",
|
||||
"type": "float",
|
||||
"description": "Temperatura",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "humidity",
|
||||
"type": "float",
|
||||
"description": "Humedad",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "pressure",
|
||||
"type": "float",
|
||||
"description": "Presión",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "float",
|
||||
"description": "Valor de CO",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "long",
|
||||
"description": "Marca temporal del valor",
|
||||
"in": "body",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/latest-values",
|
||||
"description": "Obtener los últimos valores de un dispositivo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/pollution-map",
|
||||
"description": "Obtener el mapa de contaminación de un dispositivo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/history",
|
||||
"description": "Obtener el histórico de un dispositivo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/sensors/:sensorId/values",
|
||||
"description": "Obtener los valores de un sensor específico",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/actuators/:actuator_id/status",
|
||||
"description": "Obtener el estado de un actuador",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "actuator_id",
|
||||
"type": "integer",
|
||||
"description": "ID del actuador",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status",
|
||||
"description": "Crear un nuevo dispositivo en un grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "actuatorId",
|
||||
"type": "string",
|
||||
"description": "ID del actuador",
|
||||
"in": "body",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"raw_api": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups",
|
||||
"description": "Obtener todos los grupos"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/raw/v1/groups",
|
||||
"description": "Crear un nuevo grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "groupName",
|
||||
"type": "string",
|
||||
"description": "Nombre del grupo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices",
|
||||
"description": "Obtener todos los dispositivos de un grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices",
|
||||
"description": "Crear un nuevo dispositivo en un grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceName",
|
||||
"type": "string",
|
||||
"description": "Nombre del dispositivo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId",
|
||||
"description": "Obtener un dispositivo de un grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId",
|
||||
"description": "Actualizar un dispositivo de un grupo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId/sensors",
|
||||
"description": "Obtener todos los sensores de un dispositivo",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId/sensors",
|
||||
"description": "Crear un nuevo sensor",
|
||||
"params": [
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "body",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceName",
|
||||
"type": "string",
|
||||
"description": "Nombre del dispositivo",
|
||||
"in": "body",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"description": "Obtener un sensor específico",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"description": "Actualizar un sensor específico",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/groups/:groupId/devices/:deviceId/sensors/:sensorId/values",
|
||||
"description": "Obtener los valores de un sensor",
|
||||
"params": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"description": "ID del grupo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "deviceId",
|
||||
"type": "integer",
|
||||
"description": "ID del dispositivo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sensorId",
|
||||
"type": "integer",
|
||||
"description": "ID del sensor",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/v_latest_values",
|
||||
"description": "Vista: últimos valores registrados"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/v_pollution_map",
|
||||
"description": "Vista: mapa de contaminación"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/v_sensor_history_by_device",
|
||||
"description": "Vista: histórico de sensores por dispositivo"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/raw/v1/v_sensor_values",
|
||||
"description": "Vista: valores individuales de sensores"
|
||||
}
|
||||
]
|
||||
}
|
||||
77
frontend/public/config/settings.dev.json
Normal file
77
frontend/public/config/settings.dev.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"userConfig": {
|
||||
"city": [37.38283, -5.97317]
|
||||
},
|
||||
"appConfig": {
|
||||
"endpoints": {
|
||||
"DATA_URL": "http://localhost:8081/api/raw/v1",
|
||||
"LOGIC_URL": "http://localhost:8082/api/v1",
|
||||
|
||||
"GET_GROUPS": "/groups",
|
||||
"GET_GROUP_BY_ID": "/groups/:groupId",
|
||||
"POST_GROUPS": "/groups",
|
||||
"PUT_GROUP_BY_ID": "/groups/:groupId",
|
||||
|
||||
"GET_GROUP_DEVICES": "/groups/:groupId/devices",
|
||||
"GET_DEVICE_BY_ID": "/groups/:groupId/devices/:deviceId",
|
||||
"POST_DEVICES": "/groups/:groupId/devices",
|
||||
"PUT_DEVICE_BY_ID": "/groups/:groupId/devices/:deviceId",
|
||||
|
||||
"GET_DEVICE_LATEST_VALUES": "/groups/:groupId/devices/:deviceId/latest-values",
|
||||
"GET_DEVICE_POLLUTION_MAP": "/groups/:groupId/devices/:deviceId/pollution-map",
|
||||
"GET_DEVICE_HISTORY": "/groups/:groupId/devices/:deviceId/history",
|
||||
|
||||
"GET_DEVICE_SENSORS": "/groups/:groupId/devices/:deviceId/sensors",
|
||||
"GET_SENSOR_BY_ID": "/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"POST_SENSORS": "/groups/:groupId/devices/:deviceId/sensors",
|
||||
"PUT_SENSOR_BY_ID": "/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"GET_SENSOR_VALUES": "/groups/:groupId/devices/:deviceId/sensors/:sensorId/values",
|
||||
|
||||
"GET_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators",
|
||||
"GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId",
|
||||
"POST_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators",
|
||||
"PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId",
|
||||
"GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status",
|
||||
|
||||
"VIEW_LATEST_VALUES": "/v_latest_values",
|
||||
"VIEW_POLLUTION_MAP": "/v_pollution_map",
|
||||
"VIEW_SENSOR_HISTORY": "/v_sensor_history_by_device",
|
||||
"VIEW_SENSOR_VALUES": "/v_sensor_values",
|
||||
"VIEW_CO_BY_DEVICE": "/v_co_by_device",
|
||||
"VIEW_GPS_BY_DEVICE": "/v_gps_by_device",
|
||||
"VIEW_WEATHER_BY_DEVICE": "/v_weather_by_device"
|
||||
},
|
||||
"historyChartConfig": {
|
||||
"chartOptionsDark": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": { "color": "rgba(255, 255, 255, 0.1)" },
|
||||
"ticks": { "color": "#E0E0E0" }
|
||||
},
|
||||
"y": {
|
||||
"grid": { "color": "rgba(255, 255, 255, 0.1)" },
|
||||
"ticks": { "color": "#E0E0E0" }
|
||||
}
|
||||
},
|
||||
"plugins": { "legend": { "display": false } }
|
||||
},
|
||||
"chartOptionsLight": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": { "color": "rgba(0, 0, 0, 0.1)" },
|
||||
"ticks": { "color": "#333" }
|
||||
},
|
||||
"y": {
|
||||
"grid": { "color": "rgba(0, 0, 0, 0.1)" },
|
||||
"ticks": { "color": "#333" }
|
||||
}
|
||||
},
|
||||
"plugins": { "legend": { "display": false } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"userConfig": {
|
||||
"city": [
|
||||
37.38283,
|
||||
-5.97317
|
||||
]
|
||||
},
|
||||
"appConfig": {
|
||||
"endpoints": {
|
||||
"DATA_URL": "http://localhost:8081/api/v1",
|
||||
"LOGIC_URL": "http://localhost:8082/api/v1",
|
||||
"GET_GROUPS": "/groups",
|
||||
"GET_GROUP_BY_ID": "/groups/{0}",
|
||||
"GET_GROUP_DEVICES": "/groups/{0}/devices",
|
||||
"POST_GROUPS": "/groups",
|
||||
"PUT_GROUP_BY_ID": "/groups/{0}",
|
||||
"GET_DEVICES": "/devices",
|
||||
"GET_DEVICE_BY_ID": "/devices/{0}",
|
||||
"GET_DEVICE_SENSORS": "/devices/{0}/sensors",
|
||||
"GET_DEVICE_LATEST_VALUES": "/devices/{0}/latest",
|
||||
"GET_DEVICE_POLLUTION_MAP": "/devices/{0}/pollution-map",
|
||||
"GET_DEVICE_HISTORY": "/devices/{0}/history",
|
||||
"POST_DEVICES": "/devices",
|
||||
"PUT_DEVICE_BY_ID": "/devices/{0}",
|
||||
"GET_SENSORS": "/sensors",
|
||||
"GET_SENSOR_BY_ID": "/sensors/{0}",
|
||||
"GET_SENSOR_VALUES": "/sensors/{0}/values",
|
||||
"POST_SENSORS": "/sensors",
|
||||
"PUT_SENSOR_BY_ID": "/sensors/{0}",
|
||||
"GET_ACTUATORS": "/actuators",
|
||||
"GET_ACTUATOR_BY_ID": "/actuators/{0}",
|
||||
"POST_ACTUATORS": "/actuators",
|
||||
"PUT_ACTUATOR_BY_ID": "/actuators/{0}",
|
||||
"GET_GPS_VALUES": "/gps-values",
|
||||
"GET_GPS_VALUE_BY_ID": "/gps-values/{0}",
|
||||
"POST_GPS_VALUES": "/gps-values",
|
||||
"GET_AIR_VALUES": "/air-values",
|
||||
"GET_AIR_VALUE_BY_ID": "/air-values/{0}",
|
||||
"POST_AIR_VALUES": "/air-values"
|
||||
},
|
||||
"historyChartConfig": {
|
||||
"chartOptionsDark": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": {
|
||||
"color": "rgba(255, 255, 255, 0.1)"
|
||||
},
|
||||
"ticks": {
|
||||
"color": "#E0E0E0"
|
||||
}
|
||||
},
|
||||
"y": {
|
||||
"grid": {
|
||||
"color": "rgba(255, 255, 255, 0.1)"
|
||||
},
|
||||
"ticks": {
|
||||
"color": "#E0E0E0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"legend": {
|
||||
"display": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"chartOptionsLight": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": {
|
||||
"color": "rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"ticks": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"y": {
|
||||
"grid": {
|
||||
"color": "rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
"ticks": {
|
||||
"color": "#333"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"legend": {
|
||||
"display": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
frontend/public/config/settings.prod.json
Normal file
77
frontend/public/config/settings.prod.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"userConfig": {
|
||||
"city": [37.38283, -5.97317]
|
||||
},
|
||||
"appConfig": {
|
||||
"endpoints": {
|
||||
"DATA_URL": "https://contaminus.miarma.net/api/raw/v1",
|
||||
"LOGIC_URL": "https://contaminus.miarma.net/api/v1",
|
||||
|
||||
"GET_GROUPS": "/groups",
|
||||
"GET_GROUP_BY_ID": "/groups/:groupId",
|
||||
"POST_GROUPS": "/groups",
|
||||
"PUT_GROUP_BY_ID": "/groups/:groupId",
|
||||
|
||||
"GET_GROUP_DEVICES": "/groups/:groupId/devices",
|
||||
"GET_DEVICE_BY_ID": "/groups/:groupId/devices/:deviceId",
|
||||
"POST_DEVICES": "/groups/:groupId/devices",
|
||||
"PUT_DEVICE_BY_ID": "/groups/:groupId/devices/:deviceId",
|
||||
|
||||
"GET_DEVICE_LATEST_VALUES": "/groups/:groupId/devices/:deviceId/latest-values",
|
||||
"GET_DEVICE_POLLUTION_MAP": "/groups/:groupId/devices/:deviceId/pollution-map",
|
||||
"GET_DEVICE_HISTORY": "/groups/:groupId/devices/:deviceId/history",
|
||||
|
||||
"GET_DEVICE_SENSORS": "/groups/:groupId/devices/:deviceId/sensors",
|
||||
"GET_SENSOR_BY_ID": "/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"POST_SENSORS": "/groups/:groupId/devices/:deviceId/sensors",
|
||||
"PUT_SENSOR_BY_ID": "/groups/:groupId/devices/:deviceId/sensors/:sensorId",
|
||||
"GET_SENSOR_VALUES": "/groups/:groupId/devices/:deviceId/sensors/:sensorId/values",
|
||||
|
||||
"GET_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators",
|
||||
"GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId",
|
||||
"POST_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators",
|
||||
"PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId",
|
||||
"GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status",
|
||||
|
||||
"VIEW_LATEST_VALUES": "/v_latest_values",
|
||||
"VIEW_POLLUTION_MAP": "/v_pollution_map",
|
||||
"VIEW_SENSOR_HISTORY": "/v_sensor_history_by_device",
|
||||
"VIEW_SENSOR_VALUES": "/v_sensor_values",
|
||||
"VIEW_CO_BY_DEVICE": "/v_co_by_device",
|
||||
"VIEW_GPS_BY_DEVICE": "/v_gps_by_device",
|
||||
"VIEW_WEATHER_BY_DEVICE": "/v_weather_by_device"
|
||||
},
|
||||
"historyChartConfig": {
|
||||
"chartOptionsDark": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": { "color": "rgba(255, 255, 255, 0.1)" },
|
||||
"ticks": { "color": "#E0E0E0" }
|
||||
},
|
||||
"y": {
|
||||
"grid": { "color": "rgba(255, 255, 255, 0.1)" },
|
||||
"ticks": { "color": "#E0E0E0" }
|
||||
}
|
||||
},
|
||||
"plugins": { "legend": { "display": false } }
|
||||
},
|
||||
"chartOptionsLight": {
|
||||
"responsive": true,
|
||||
"maintainAspectRatio": false,
|
||||
"scales": {
|
||||
"x": {
|
||||
"grid": { "color": "rgba(0, 0, 0, 0.1)" },
|
||||
"ticks": { "color": "#333" }
|
||||
},
|
||||
"y": {
|
||||
"grid": { "color": "rgba(0, 0, 0, 0.1)" },
|
||||
"ticks": { "color": "#333" }
|
||||
}
|
||||
},
|
||||
"plugins": { "legend": { "display": false } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/fonts/LEDBOARD.ttf
Normal file
BIN
frontend/public/fonts/LEDBOARD.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/OpenSans.ttf
Normal file
BIN
frontend/public/fonts/OpenSans.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansBold.ttf
Normal file
BIN
frontend/public/fonts/ProductSansBold.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansBoldItalic.ttf
Normal file
BIN
frontend/public/fonts/ProductSansBoldItalic.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansItalic.ttf
Normal file
BIN
frontend/public/fonts/ProductSansItalic.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansRegular.ttf
Normal file
BIN
frontend/public/fonts/ProductSansRegular.ttf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/images/logo-dark.png
Normal file
BIN
frontend/public/images/logo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/public/images/logo-light.png
Normal file
BIN
frontend/public/images/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 412 KiB |
BIN
frontend/public/images/voro.png
Normal file
BIN
frontend/public/images/voro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/public/voronoi_sevilla_geovoronoi.geojson
Normal file
1
frontend/public/voronoi_sevilla_geovoronoi.geojson
Normal file
File diff suppressed because one or more lines are too long
34
frontend/src/App.jsx
Normal file
34
frontend/src/App.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import '@/css/App.css'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
import Dashboard from '@/pages/Dashboard.jsx'
|
||||
import Groups from '@/pages/Groups.jsx'
|
||||
import Header from '@/components/layout/Header.jsx'
|
||||
import GroupView from '@/pages/GroupView.jsx'
|
||||
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import ContentWrapper from '@/components/layout/ContentWrapper'
|
||||
import Docs from '@/pages/Docs'
|
||||
import FloatingMenu from '@/components/layout/FloatingMenu'
|
||||
|
||||
const App = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FloatingMenu />
|
||||
<Header subtitle='Midiendo la calidad del aire y las calles en Sevilla 🌿🚛' />
|
||||
<ContentWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<Groups />} />
|
||||
<Route path="/groups/:groupId" element={<GroupView />} />
|
||||
<Route path="/groups/:groupId/devices/:deviceId" element={<Dashboard />} />
|
||||
<Route path="/docs" element={<Docs url={"/apidoc.json"} />} />
|
||||
</Routes>
|
||||
</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
14
frontend/src/api/axiosInstance.js
Normal file
14
frontend/src/api/axiosInstance.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import axios from "axios";
|
||||
|
||||
const createAxiosInstance = (baseURL, token) => {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default createAxiosInstance;
|
||||
74
frontend/src/components/ApiDocs.jsx
Normal file
74
frontend/src/components/ApiDocs.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Accordion } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
const ApiDocs = ({ json }) => {
|
||||
if (!json) return <p className="text-muted">No hay documentación disponible.</p>;
|
||||
|
||||
const renderEndpoints = (endpoints) => (
|
||||
<Accordion className='overflow-auto'>
|
||||
{endpoints.map((ep, index) => (
|
||||
<Accordion.Item eventKey={index.toString()} key={index}>
|
||||
<Accordion.Header className='d-flex align-items-center flex-wrap'>
|
||||
<span className={`badge bg-${getMethodColor(ep.method)} me-2 text-uppercase`}>{ep.method}</span>
|
||||
<code className='text-break flex-grow-1'>{ep.path}</code>
|
||||
</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{ep.description && <p className="mb-2">{ep.description}</p>}
|
||||
|
||||
{ep.params?.length > 0 && (
|
||||
<div className="d-flex flex-column gap-2 mt-3">
|
||||
{ep.params.map((param, i) => (
|
||||
<div key={i} className="bg-light border rounded px-3 py-2">
|
||||
<div className="d-flex justify-content-between flex-wrap mb-1">
|
||||
<strong>{param.name}</strong>
|
||||
<span className="badge bg-secondary">{param.in}</span>
|
||||
</div>
|
||||
<div className="small text-muted">
|
||||
<div><strong>Tipo:</strong> {param.type}</div>
|
||||
<div><strong>¿Requerido?:</strong> {param.required ? 'Sí' : 'No'}</div>
|
||||
{param.description && <div><strong>Descripción:</strong> {param.description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container p-4 bg-white rounded-4 border">
|
||||
<h1 className="fw-bold mb-5 text-dark">{json.name} <small className="text-muted fs-5">v{json.version}</small></h1>
|
||||
|
||||
<h3 className="mb-3 text-dark">API de Lógica</h3>
|
||||
{renderEndpoints(json.logic_api)}
|
||||
|
||||
<h3 className="mb-3 text-dark mt-5">API de Datos (Raw)</h3>
|
||||
{renderEndpoints(json.raw_api)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getMethodColor = (method) => {
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET': return 'success';
|
||||
case 'POST': return 'primary';
|
||||
case 'PUT': return 'warning';
|
||||
case 'DELETE': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
ApiDocs.propTypes = {
|
||||
json: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
logic_api: PropTypes.array,
|
||||
raw_api: PropTypes.array
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default ApiDocs;
|
||||
@@ -1,70 +0,0 @@
|
||||
import '../css/App.css'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
import Home from '../pages/Home.jsx'
|
||||
import Dashboard from '../pages/Dashboard.jsx'
|
||||
import MenuButton from './MenuButton.jsx'
|
||||
import SideMenu from './SideMenu.jsx'
|
||||
import ThemeButton from '../components/ThemeButton.jsx'
|
||||
import Header from '../components/Header.jsx'
|
||||
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* App.jsx
|
||||
*
|
||||
* Este archivo define el componente App, que es el componente principal de la aplicación.
|
||||
*
|
||||
* Importaciones:
|
||||
* - '../css/App.css': Archivo CSS que contiene los estilos globales de la aplicación.
|
||||
* - 'leaflet/dist/leaflet.css': Archivo CSS que contiene los estilos para los mapas de Leaflet.
|
||||
* - 'bootstrap/dist/css/bootstrap.min.css': Archivo CSS que contiene los estilos de Bootstrap.
|
||||
* - 'bootstrap/dist/js/bootstrap.bundle.min.js': Archivo JS que contiene los scripts de Bootstrap.
|
||||
* - Header: Componente que representa el encabezado de la página.
|
||||
* - Home: Componente que representa la página principal de la aplicación.
|
||||
* - MenuButton: Componente que representa el botón del menú lateral.
|
||||
* - SideMenu: Componente que representa el menú lateral.
|
||||
* - ThemeButton: Componente que representa el botón de cambio de tema.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - App: Componente principal que renderiza la página Home.
|
||||
* - Planea añadir un React Router en el futuro.
|
||||
* - El componente Header muestra el título y subtítulo de la página.
|
||||
* - El componente MenuButton muestra un botón para abrir el menú lateral.
|
||||
* - El componente SideMenu muestra un menú lateral con opciones de navegación.
|
||||
* - El componente ThemeButton muestra un botón para cambiar el tema de la aplicación.
|
||||
* - El componente Home contiene el contenido principal de la aplicación.
|
||||
*
|
||||
*/
|
||||
|
||||
const App = () => {
|
||||
const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
|
||||
|
||||
const toggleSideMenu = () => {
|
||||
setIsSideMenuOpen(!isSideMenuOpen);
|
||||
};
|
||||
|
||||
const closeSideMenu = () => {
|
||||
setIsSideMenuOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton onClick={toggleSideMenu} />
|
||||
<SideMenu isOpen={isSideMenuOpen} onClose={toggleSideMenu} />
|
||||
<ThemeButton />
|
||||
<div className={isSideMenuOpen ? 'blur m-0 p-0' : 'm-0 p-0'} onClick={closeSideMenu}>
|
||||
<Header title='Contamin' subtitle='Midiendo la calidad del aire y las calles en Sevilla 🌿🚛' />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/dashboard/:deviceId" element={<Dashboard />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,84 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "../css/Card.css";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
/**
|
||||
* Card.jsx
|
||||
*
|
||||
* Este archivo define el componente Card, que representa una tarjeta individual con un título, estado y contenido.
|
||||
*
|
||||
* Importaciones:
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
* - useState, useEffect, useRef: Hooks de React para manejar estados, efectos secundarios y referencias.
|
||||
* - "../css/Card.css": Archivo CSS que contiene los estilos para las tarjetas.
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - Card: Componente que renderiza una tarjeta con un título, estado y contenido.
|
||||
* - Utiliza el hook `useTheme` para aplicar la clase correspondiente al tema actual.
|
||||
* - Ajusta el título de la tarjeta según el tamaño de la tarjeta.
|
||||
*
|
||||
* PropTypes:
|
||||
* - Card espera una propiedad `title` que es un string requerido.
|
||||
* - Card espera una propiedad `status` que es un string requerido.
|
||||
* - Card espera una propiedad `children` que es un nodo de React requerido.
|
||||
* - Card espera una propiedad `styleMode` que es opcional y puede ser "override" o una cadena vacía.
|
||||
* - Card espera una propiedad `className` que es un string opcional.
|
||||
*
|
||||
*/
|
||||
|
||||
const Card = ({ title, status, children, styleMode, className, titleIcon }) => {
|
||||
const cardRef = useRef(null);
|
||||
const [shortTitle, setShortTitle] = useState(title);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const checkSize = () => {
|
||||
if (cardRef.current) {
|
||||
const width = cardRef.current.offsetWidth;
|
||||
if (width < 300 && title.length > 15) {
|
||||
setShortTitle(title.slice(0, 10) + ".");
|
||||
} else {
|
||||
setShortTitle(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSize();
|
||||
window.addEventListener("resize", checkSize);
|
||||
return () => window.removeEventListener("resize", checkSize);
|
||||
}, [title]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={styleMode === "override" ? `${className}` :
|
||||
`col-xl-3 col-sm-6 d-flex flex-column align-items-center p-3 card-container ${className}`}
|
||||
>
|
||||
<div className={`card p-3 w-100 ${theme}`}>
|
||||
<h3 className="text-center">
|
||||
{titleIcon}
|
||||
{shortTitle}
|
||||
</h3>
|
||||
<div className="card-content">{children}</div>
|
||||
{status ? <span className="status text-center mt-2">{status}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
styleMode: PropTypes.oneOf(["override", ""]),
|
||||
className: PropTypes.string,
|
||||
titleIcon: PropTypes.node,
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
styleMode: "",
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,46 +0,0 @@
|
||||
import Card from "./Card.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* CardContainer.jsx
|
||||
*
|
||||
* Este archivo define el componente CardContainer, que actúa como contenedor para múltiples componentes Card.
|
||||
*
|
||||
* Importaciones:
|
||||
* - Card: Componente que representa una tarjeta individual.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - CardContainer: Componente que renderiza un contenedor (`div`) con una fila de tarjetas (`Card`).
|
||||
* - Utiliza `props.cards` para mapear y renderizar cada tarjeta con su contenido.
|
||||
*
|
||||
* PropTypes:
|
||||
* - CardContainer espera una propiedad `cards` que es un array de objetos con las propiedades `title`, `content` y `status`.
|
||||
* - CardContainer espera una propiedad `className` que es un string opcional.
|
||||
*
|
||||
*/
|
||||
|
||||
const CardContainer = ({ cards, className }) => {
|
||||
return (
|
||||
<div className={`row justify-content-center g-0 ${className}`}>
|
||||
{cards.map((card, index) => (
|
||||
<Card key={index} title={card.title} status={card.status} styleMode={card.styleMode} className={card.className} titleIcon={card.titleIcon}>
|
||||
<p className="card-text text-center">{card.content}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardContainer.propTypes = {
|
||||
cards: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CardContainer;
|
||||
@@ -1,41 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import '../css/Header.css';
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
/**
|
||||
* Header.jsx
|
||||
*
|
||||
* Este archivo define el componente Header, que muestra el encabezado de la página con un título y un subtítulo.
|
||||
*
|
||||
* Importaciones:
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
* - "../css/Header.css": Archivo CSS que contiene los estilos para el encabezado.
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - Header: Componente que renderiza un encabezado con un título y un subtítulo.
|
||||
* - Utiliza el hook `useTheme` para aplicar la clase correspondiente al tema actual.
|
||||
*
|
||||
* PropTypes:
|
||||
* - Header espera una propiedad `title` que es un string requerido.
|
||||
* - Header espera una propiedad `subtitle` que es un string opcional.
|
||||
*
|
||||
*/
|
||||
|
||||
const Header = (props) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className={`justify-content-center text-center mb-4 ${theme}`}>
|
||||
<h1>{props.title}</h1>
|
||||
<p className='subtitle'>{props.subtitle}</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -1,43 +1,18 @@
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { Chart as ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Filler } from "chart.js";
|
||||
import CardContainer from "./CardContainer";
|
||||
import "../css/HistoryCharts.css";
|
||||
import CardContainer from "./layout/CardContainer";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useTheme } from "../contexts/ThemeContext.jsx";
|
||||
import { DataProvider, useData } from "../contexts/DataContext.jsx";
|
||||
import { useConfig } from "../contexts/ConfigContext.jsx";
|
||||
|
||||
/**
|
||||
* HistoryCharts.jsx
|
||||
*
|
||||
* Este archivo define el componente HistoryCharts, que muestra gráficos históricos de datos obtenidos de sensores.
|
||||
*
|
||||
* Importaciones:
|
||||
* - Line: Componente de react-chartjs-2 para renderizar gráficos de líneas.
|
||||
* - ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Filler: Módulos de chart.js para configurar y registrar los elementos del gráfico.
|
||||
* - CardContainer: Componente que actúa como contenedor para las tarjetas.
|
||||
* - "../css/HistoryCharts.css": Archivo CSS que contiene los estilos para los gráficos históricos.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
|
||||
* - useConfig: Hook personalizado para acceder al contexto de configuración.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - HistoryCharts: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
|
||||
* - Muestra mensajes de carga y error según el estado de la configuración.
|
||||
* - HistoryChartsContent: Componente que procesa los datos obtenidos y renderiza los gráficos históricos.
|
||||
* - Utiliza el hook `useData` para acceder a los datos de sensores.
|
||||
* - Renderiza gráficos de líneas con diferentes colores según el tipo de dato (temperatura, humedad, contaminación).
|
||||
*
|
||||
* PropTypes:
|
||||
* - HistoryChartsContent espera propiedades `options` (objeto), `timeLabels` (array) y `data` (array).
|
||||
*
|
||||
*/
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { DataProvider } from "@/context/DataContext.jsx";
|
||||
import { useDataContext } from "@/hooks/useDataContext";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler);
|
||||
|
||||
const HistoryCharts = () => {
|
||||
const HistoryCharts = ({ groupId, deviceId }) => {
|
||||
const { config, configLoading, configError } = useConfig();
|
||||
|
||||
if (configLoading) return <p>Cargando configuración...</p>;
|
||||
@@ -45,12 +20,15 @@ const HistoryCharts = () => {
|
||||
if (!config) return <p>Configuración no disponible.</p>;
|
||||
|
||||
const BASE = config.appConfig.endpoints.LOGIC_URL;
|
||||
const ENDPOINT = config.appConfig.endpoints.sensors;
|
||||
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_HISTORY;
|
||||
const endp = ENDPOINT
|
||||
.replace(':groupId', groupId)
|
||||
.replace(':deviceId', deviceId);
|
||||
|
||||
const reqConfig = {
|
||||
baseUrl: `${BASE}/${ENDPOINT}`,
|
||||
params: {}
|
||||
}
|
||||
baseUrl: `${BASE}${endp}`,
|
||||
params: {}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataProvider config={reqConfig}>
|
||||
@@ -61,62 +39,102 @@ const HistoryCharts = () => {
|
||||
|
||||
const HistoryChartsContent = () => {
|
||||
const { config } = useConfig();
|
||||
const { data, loading } = useData();
|
||||
const { data, loading, error } = useDataContext();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const optionsDark = config?.appConfig?.historyChartConfig?.chartOptionsDark ?? {};
|
||||
const optionsLight = config?.appConfig?.historyChartConfig?.chartOptionsLight ?? {};
|
||||
const options = theme === "dark" ? optionsDark : optionsLight;
|
||||
|
||||
const currentHour = new Date().getHours();
|
||||
const timeLabels = [
|
||||
`${currentHour - 3}:00`, `${currentHour - 2}:00`, `${currentHour - 1}:00`, `${currentHour}:00`, `${currentHour + 1}:00`, `${currentHour + 2}:00`, `${currentHour + 3}:00`
|
||||
]
|
||||
|
||||
if (loading) return <p>Cargando datos...</p>;
|
||||
if (error) return <p>Datos no disponibles.</p>;
|
||||
|
||||
const temperatureData = [];
|
||||
const humidityData = [];
|
||||
const pollutionLevels = [];
|
||||
const grouped = {
|
||||
temperature: [],
|
||||
humidity: [],
|
||||
pressure: [],
|
||||
carbonMonoxide: []
|
||||
};
|
||||
|
||||
const threeDaysAgo = Date.now() - (3 * 24 * 60 * 60 * 1000); // hace 3 días en ms
|
||||
const isRecent = (timestamp) => (timestamp * 1000) >= threeDaysAgo;
|
||||
|
||||
data?.forEach(sensor => {
|
||||
if (sensor.value != null) {
|
||||
if (sensor.sensor_type === "MQ-135") {
|
||||
pollutionLevels.push(sensor.value);
|
||||
} else if (sensor.sensor_type === "DHT-11") {
|
||||
temperatureData.push(sensor.value);
|
||||
humidityData.push(sensor.value);
|
||||
}
|
||||
if (
|
||||
sensor.value != null &&
|
||||
grouped[sensor.valueType] &&
|
||||
isRecent(sensor.timestamp)
|
||||
) {
|
||||
grouped[sensor.valueType].push({
|
||||
timestamp: sensor.timestamp * 1000,
|
||||
value: sensor.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sortAndExtract = (entries) => {
|
||||
const sorted = entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
const labels = sorted.map(e =>
|
||||
new Date(e.timestamp).toLocaleTimeString('es-ES', {
|
||||
timeZone: 'UTC',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
);
|
||||
|
||||
const values = sorted.map(e => e.value);
|
||||
return { labels, values };
|
||||
};
|
||||
|
||||
|
||||
const temp = sortAndExtract(grouped.temperature);
|
||||
const hum = sortAndExtract(grouped.humidity);
|
||||
const press = sortAndExtract(grouped.pressure);
|
||||
const co = sortAndExtract(grouped.carbonMonoxide);
|
||||
|
||||
const timeLabels = temp.labels.length ? temp.labels : hum.labels.length ? hum.labels : co.labels.length ? co.labels : ["Sin datos"];
|
||||
|
||||
const historyData = [
|
||||
{ title: "🌡️ Temperatura", data: temperatureData.length ? temperatureData : [0], borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" },
|
||||
{ title: "💧 Humedad", data: humidityData.length ? humidityData : [0], borderColor: "#00D4FF", backgroundColor: "rgba(0, 212, 255, 0.2)" },
|
||||
{ title: "☁️ Contaminación", data: pollutionLevels.length ? pollutionLevels : [0], borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" }
|
||||
{ title: "🌡️ Temperatura", data: temp.values, borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" },
|
||||
{ title: "💦 Humedad", data: hum.values, borderColor: "#00D4FF", backgroundColor: "rgba(0, 212, 255, 0.2)" },
|
||||
{ title: "⏲ Presión", data: press.values, borderColor: "#B12424", backgroundColor: "rgba(255, 0, 0, 0.2)" },
|
||||
{ title: "☁️ Contaminación", data: co.values, borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" }
|
||||
];
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
cards={historyData.map(({ title, data, borderColor, backgroundColor }) => ({
|
||||
title,
|
||||
content: (
|
||||
<Line
|
||||
data={{
|
||||
labels: timeLabels,
|
||||
datasets: [{ data, borderColor, backgroundColor, fill: true, tension: 0.4 }]
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
),
|
||||
styleMode: "override",
|
||||
className: "col-lg-4 col-xxs-12 d-flex flex-column align-items-center p-3 card-container"
|
||||
}))}
|
||||
className=""
|
||||
/>
|
||||
<>
|
||||
<CardContainer
|
||||
cards={historyData.map(({ title, data, borderColor, backgroundColor }) => ({
|
||||
title,
|
||||
content: (
|
||||
<Line
|
||||
data={{
|
||||
labels: timeLabels,
|
||||
datasets: [{ data, borderColor, backgroundColor, fill: true, tension: 0.4 }]
|
||||
}}
|
||||
options={options}
|
||||
style={{ minHeight: "250px", width: '100%'}}
|
||||
/>
|
||||
),
|
||||
styleMode: "override",
|
||||
className: "col-lg-6 col-xxs-12 d-flex flex-column align-items-center",
|
||||
style: { minHeight: "250px", width: '100%' }
|
||||
}))}
|
||||
/>
|
||||
<span className="m-0 p-0 d-flex align-items-center justify-content-center">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="me-2" />
|
||||
<p className="m-0 p-0">El historial muestra datos de los últimos 3 días, el mapa del día actual, y arriba del todo los datos son los últimos recogidos independientemente de la fecha</p>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
HistoryCharts.propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
HistoryChartsContent.propTypes = {
|
||||
options: PropTypes.object,
|
||||
timeLabels: PropTypes.array,
|
||||
|
||||
10
frontend/src/components/LoadingIcon.jsx
Normal file
10
frontend/src/components/LoadingIcon.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const LoadingIcon = () => {
|
||||
return (
|
||||
<FontAwesomeIcon icon={faSpinner} className='fa-spin fa-lg' />
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingIcon;
|
||||
@@ -1,35 +0,0 @@
|
||||
import "../css/MenuButton.css";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons';
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/** ⚠️ EN PRUEBAS ⚠️
|
||||
* MenuButton.jsx
|
||||
*
|
||||
* Este archivo define el componente MenuButton, que muestra un botón de menú con un icono de barras.
|
||||
*
|
||||
* Importaciones:
|
||||
* - "../css/MenuButton.css": Archivo CSS que contiene los estilos para el botón de menú.
|
||||
* - FontAwesomeIcon, faBars: Componentes e iconos de FontAwesome para mostrar el icono de barras.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - MenuButton: Componente que renderiza un botón con un icono de barras.
|
||||
* - Utiliza la propiedad `onClick` para manejar el evento de clic del botón.
|
||||
*
|
||||
* PropTypes:
|
||||
* - MenuButton espera una propiedad `onClick` que es una función requerida.
|
||||
* ⚠️ EN PRUEBAS ⚠️ **/
|
||||
|
||||
export default function MenuButton({ onClick }) {
|
||||
return (
|
||||
<button className="menuBtn" onClick={onClick}>
|
||||
<FontAwesomeIcon icon={faBars} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
MenuButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1,64 +1,16 @@
|
||||
import { MapContainer, TileLayer, Circle, Popup } from 'react-leaflet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useConfig } from '../contexts/ConfigContext.jsx';
|
||||
import { useConfig } from '@/hooks/useConfig.js';
|
||||
|
||||
import { DataProvider } from '../contexts/DataContext.jsx';
|
||||
import { useData } from '../contexts/DataContext.jsx';
|
||||
import { DataProvider } from '@/context/DataContext.jsx';
|
||||
import { useDataContext } from '@/hooks/useDataContext';
|
||||
|
||||
/**
|
||||
* PollutionMap.jsx
|
||||
*
|
||||
* Este archivo define el componente PollutionMap, que muestra un mapa con los niveles de contaminación en diferentes ubicaciones.
|
||||
*
|
||||
* Importaciones:
|
||||
* - MapContainer, TileLayer, Circle, Popup: Componentes de react-leaflet para renderizar el mapa y los círculos de contaminación.
|
||||
* - useConfig: Hook personalizado para acceder al contexto de configuración.
|
||||
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - PollutionMap: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
|
||||
* - Muestra mensajes de carga y error según el estado de la configuración.
|
||||
* - PollutionMapContent: Componente que procesa los datos obtenidos y renderiza los círculos de contaminación en el mapa.
|
||||
* - Utiliza el hook `useData` para acceder a los datos de sensores.
|
||||
* - Renderiza círculos de diferentes colores y tamaños según el nivel de contaminación.
|
||||
*
|
||||
*/
|
||||
import L from "leaflet";
|
||||
import "leaflet.heat";
|
||||
|
||||
const PollutionCircles = ({ data }) => {
|
||||
return data.map(({ lat, lng, level }, index) => {
|
||||
const baseColor = level < 20 ? '#00FF85' : level < 60 ? '#FFA500' : '#FF0000';
|
||||
const steps = 4;
|
||||
const maxRadius = 400;
|
||||
const stepSize = maxRadius / steps;
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{[...Array(steps)].map((_, i) => {
|
||||
const radius = stepSize * (i + 1);
|
||||
const opacity = 0.6 * ((i + 1) / steps);
|
||||
return (
|
||||
<Circle
|
||||
key={`${index}-${i}`}
|
||||
center={[lat, lng]}
|
||||
pathOptions={{ color: baseColor, fillColor: baseColor, fillOpacity: opacity, weight: 1 }}
|
||||
radius={radius}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Circle
|
||||
center={[lat, lng]}
|
||||
pathOptions={{ color: baseColor, fillColor: baseColor, fillOpacity: 0.8, weight: 2 }}
|
||||
radius={50}
|
||||
>
|
||||
<Popup>Contaminación: {level} µg/m³</Popup>
|
||||
</Circle>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const PollutionMap = ({ deviceId }) => {
|
||||
const PollutionMap = ({ groupId, deviceId }) => {
|
||||
const { config, configLoading, configError } = useConfig();
|
||||
|
||||
if (configLoading) return <p>Cargando configuración...</p>;
|
||||
@@ -67,12 +19,14 @@ const PollutionMap = ({ deviceId }) => {
|
||||
|
||||
const BASE = config.appConfig.endpoints.LOGIC_URL;
|
||||
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_POLLUTION_MAP;
|
||||
let endp = ENDPOINT.replace('{0}', deviceId);
|
||||
const endp = ENDPOINT
|
||||
.replace(':groupId', groupId)
|
||||
.replace(':deviceId', deviceId);
|
||||
|
||||
const reqConfig = {
|
||||
baseUrl: `${BASE}/${endp}`,
|
||||
params: {}
|
||||
}
|
||||
baseUrl: `${BASE}${endp}`,
|
||||
params: {}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataProvider config={reqConfig}>
|
||||
@@ -83,44 +37,167 @@ const PollutionMap = ({ deviceId }) => {
|
||||
|
||||
const PollutionMapContent = () => {
|
||||
const { config, configLoading, configError } = useConfig();
|
||||
const { data, dataLoading, dataError } = useData();
|
||||
const { data, dataLoading, dataError } = useDataContext();
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const voronoiLayerRef = useRef(null);
|
||||
const [showVoronoi, setShowVoronoi] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !config) return;
|
||||
|
||||
const isToday = (timestamp) => {
|
||||
const today = new Date();
|
||||
const date = new Date(timestamp * 1000);
|
||||
return (
|
||||
today.getFullYear() === date.getFullYear() &&
|
||||
today.getMonth() === date.getMonth() &&
|
||||
today.getDate() === date.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
const mapContainer = document.getElementById("map");
|
||||
if (!mapContainer) return;
|
||||
|
||||
const getFillColor = (feature) => {
|
||||
const index = feature.properties.groupId || Math.floor(Math.random() * 10);
|
||||
const colors = [
|
||||
"#EF5350", // rojo coral
|
||||
"#EC407A", // rosa fucsia
|
||||
"#AB47BC", // púrpura
|
||||
"#7E57C2", // violeta oscuro
|
||||
"#5C6BC0", // azul medio
|
||||
"#42A5F5", // azul claro
|
||||
"#29B6F6", // celeste intenso
|
||||
"#26C6DA", // azul verdoso
|
||||
"#26A69A", // verde azulado
|
||||
"#66BB6A", // verde hoja
|
||||
"#9CCC65", // verde lima
|
||||
"#D4E157", // lima amarillenta
|
||||
"#FFEE58", // amarillo mostaza
|
||||
"#FFCA28", // amarillo dorado
|
||||
"#FFA726", // naranja quemado
|
||||
"#FF7043", // naranja rojizo
|
||||
"#8D6E63", // marrón topo
|
||||
"#78909C" // gris azulado
|
||||
];
|
||||
|
||||
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
|
||||
const SEVILLA = config.userConfig.city;
|
||||
|
||||
const map = L.map(mapContainer).setView(SEVILLA, 12);
|
||||
mapRef.current = map;
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
const points = data
|
||||
.filter(p => isToday(p.timestamp))
|
||||
.map(p => [p.lat, p.lon, p.carbonMonoxide]);
|
||||
|
||||
L.heatLayer(points, { radius: 25 }).addTo(map);
|
||||
|
||||
fetch("/voronoi_sevilla_geovoronoi.geojson")
|
||||
.then(res => res.json())
|
||||
.then(geojson => {
|
||||
const voronoiLayer = L.geoJSON(geojson, {
|
||||
style: (feature) => ({
|
||||
color: "#007946",
|
||||
weight: 1.0,
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.3,
|
||||
fillColor: getFillColor(feature),
|
||||
dashArray: '5, 5'
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
voronoiLayerRef.current = voronoiLayer;
|
||||
|
||||
if (showVoronoi) {
|
||||
voronoiLayer.addTo(map);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error cargando el GeoJSON:", err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
map.remove();
|
||||
};
|
||||
}, [data, config]);
|
||||
|
||||
const toggleVoronoi = () => {
|
||||
const map = mapRef.current;
|
||||
const voronoiLayer = voronoiLayerRef.current;
|
||||
|
||||
if (!map || !voronoiLayer) return;
|
||||
|
||||
if (map.hasLayer(voronoiLayer)) {
|
||||
map.removeLayer(voronoiLayer);
|
||||
setShowVoronoi(false);
|
||||
} else {
|
||||
voronoiLayer.addTo(map);
|
||||
setShowVoronoi(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (configLoading) return <p>Cargando configuración...</p>;
|
||||
if (configError) return <p>Error al cargar configuración: {configError}</p>;
|
||||
if (!config) return <p>Configuración no disponible.</p>;
|
||||
|
||||
if (dataLoading) return <p>Cargando datos...</p>;
|
||||
if (dataError) return <p>Error al cargar datos: {configError}</p>;
|
||||
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
|
||||
if (!data) return <p>Datos no disponibles.</p>;
|
||||
|
||||
const SEVILLA = config?.userConfig.city;
|
||||
|
||||
const pollutionData = data.map((measure) => ({
|
||||
lat: measure.lat,
|
||||
lng: measure.lon,
|
||||
level: measure.carbonMonoxide
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='p-3'>
|
||||
<MapContainer center={SEVILLA} zoom={13} scrollWheelZoom={false} style={mapStyles}>
|
||||
<TileLayer
|
||||
attribution='© Contribuidores de <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<PollutionCircles data={pollutionData} />
|
||||
</MapContainer>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div id="map" className='rounded-4' style={{ height: "60vh" }}></div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "80px",
|
||||
left: "10px",
|
||||
zIndex: 1000,
|
||||
border: "2px solid rgba(0,0,0,0.2)",
|
||||
padding: "0",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
borderRadius: "4px",
|
||||
backgroundClip: "padding-box",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={toggleVoronoi}
|
||||
style={{
|
||||
|
||||
zIndex: 1000,
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
padding: "0",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: "2px",
|
||||
border: "none",
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.backgroundColor = "#f4f4f4"}
|
||||
onMouseOut={(e) => e.currentTarget.style.backgroundColor = "#ffffff"}
|
||||
>
|
||||
<img src='/images/voro.png' width={30} height={30} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStyles = {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
borderRadius: '20px'
|
||||
};
|
||||
|
||||
PollutionMap.propTypes = {
|
||||
groupId: PropTypes.number.isRequired,
|
||||
deviceId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import "../css/SideMenu.css";
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTimes, faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { DataProvider } from '../contexts/DataContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
|
||||
import { useConfig } from '../contexts/ConfigContext';
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
import Card from './Card';
|
||||
|
||||
/** ⚠️ EN PRUEBAS ⚠️
|
||||
* SideMenu.jsx
|
||||
*
|
||||
* Este archivo define el componente SideMenu, que muestra un menú lateral con enlaces de navegación.
|
||||
*
|
||||
* Importaciones:
|
||||
* - "../css/SideMenu.css": Archivo CSS que contiene los estilos para el menú lateral.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
* - FontAwesomeIcon, faTimes: Componentes e iconos de FontAwesome para mostrar el icono de cerrar.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - SideMenu: Componente que renderiza un menú lateral con enlaces de navegación.
|
||||
* - Utiliza la propiedad `isOpen` para determinar si el menú debe estar visible.
|
||||
* - Utiliza la propiedad `onClose` para manejar el evento de cierre del menú.
|
||||
*
|
||||
* PropTypes:
|
||||
* - SideMenu espera una propiedad `isOpen` que es un booleano requerido.
|
||||
* - SideMenu espera una propiedad `onClose` que es una función requerida.
|
||||
* ⚠️ EN PRUEBAS ⚠️ **/
|
||||
|
||||
const SideMenu = ({ isOpen, onClose }) => {
|
||||
const { config, configLoading, configError } = useConfig();
|
||||
|
||||
if (configLoading) return <p>Cargando configuración...</p>;
|
||||
if (configError) return <p>Error al cargar configuración: {configError}</p>;
|
||||
if (!config) return <p>Configuración no disponible.</p>;
|
||||
|
||||
const BASE = config.appConfig.endpoints.DATA_URL;
|
||||
const ENDPOINT = config.appConfig.endpoints.GET_DEVICES;
|
||||
|
||||
const reqConfig = {
|
||||
baseUrl: `${BASE}/${ENDPOINT}`,
|
||||
params: {}
|
||||
}
|
||||
|
||||
return (
|
||||
<DataProvider config={reqConfig}>
|
||||
<SideMenuContent isOpen={isOpen} onClose={onClose} />
|
||||
</DataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const SideMenuContent = ({ isOpen, onClose }) => {
|
||||
const { data, dataLoading, dataError } = useData();
|
||||
const { theme } = useTheme();
|
||||
|
||||
if (dataLoading) return <p>Cargando datos...</p>;
|
||||
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
|
||||
if (!data) return <p>Datos no disponibles.</p>;
|
||||
|
||||
return (
|
||||
<div className={`side-menu ${isOpen ? 'open' : ''} ${theme}`}>
|
||||
<button className="home-btn" onClick={() => window.location.href = '/'}>
|
||||
<FontAwesomeIcon icon={faHome} />
|
||||
</button>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
<hr className="separation w-100"></hr>
|
||||
<div className="d-flex flex-column gap-3 mt-5">
|
||||
{data.map(device => {
|
||||
return (
|
||||
<a href={`/dashboard/${device.deviceId}`} key={device.deviceId} style={{ textDecoration: 'none' }}>
|
||||
<Card
|
||||
title={device.deviceName}
|
||||
status={`ID: ${device.deviceId}`}
|
||||
styleMode={"override"}
|
||||
className={"col-12"}
|
||||
>
|
||||
{[]}
|
||||
</Card>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SideMenu.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
SideMenuContent.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default SideMenu;
|
||||
@@ -1,39 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import CardContainer from './CardContainer';
|
||||
import CardContainer from './layout/CardContainer';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCloud, faClock, faTemperature0, faWater } from '@fortawesome/free-solid-svg-icons';
|
||||
import { DataProvider } from '@/context/DataContext';
|
||||
import { useDataContext } from '@/hooks/useDataContext';
|
||||
|
||||
import { DataProvider } from '../contexts/DataContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
import { useConfig } from '@/hooks/useConfig.js';
|
||||
|
||||
import { useConfig } from '../contexts/ConfigContext';
|
||||
import { timestampToTime, formatTime } from '../util/date.js';
|
||||
|
||||
/**
|
||||
* SummaryCards.jsx
|
||||
*
|
||||
* Este archivo define el componente SummaryCards, que muestra tarjetas resumen con información relevante obtenida de sensores.
|
||||
*
|
||||
* Importaciones:
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
* - CardContainer: Componente que actúa como contenedor para las tarjetas.
|
||||
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
|
||||
* - useConfig: Hook personalizado para acceder al contexto de configuración.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - SummaryCards: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
|
||||
* - Muestra mensajes de carga y error según el estado de la configuración.
|
||||
* - SummaryCardsContent: Componente que procesa los datos obtenidos y actualiza el contenido de las tarjetas.
|
||||
* - Utiliza el hook `useData` para acceder a los datos de sensores.
|
||||
* - Actualiza el contenido y estado de las tarjetas según los datos obtenidos.
|
||||
*
|
||||
* PropTypes:
|
||||
* - SummaryCards espera una propiedad `data` que es un array.
|
||||
*
|
||||
*/
|
||||
|
||||
const SummaryCards = ({ deviceId }) => {
|
||||
const SummaryCards = ({ groupId, deviceId }) => {
|
||||
const { config, configLoading, configError } = useConfig();
|
||||
|
||||
if (configLoading) return <p>Cargando configuración...</p>;
|
||||
@@ -42,49 +15,88 @@ const SummaryCards = ({ deviceId }) => {
|
||||
|
||||
const BASE = config.appConfig.endpoints.LOGIC_URL;
|
||||
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_LATEST_VALUES;
|
||||
const endp = ENDPOINT.replace('{0}', deviceId);
|
||||
const endp = ENDPOINT
|
||||
.replace(':groupId', groupId)
|
||||
.replace(':deviceId', deviceId);
|
||||
|
||||
const reqConfig = {
|
||||
baseUrl: `${BASE}/${endp}`,
|
||||
baseUrl: `${BASE}${endp}`,
|
||||
params: {}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataProvider config={reqConfig}>
|
||||
<SummaryCardsContent deviceId={deviceId} />
|
||||
<SummaryCardsContent />
|
||||
</DataProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SummaryCardsContent = () => {
|
||||
const { data, dataLoading, dataError } = useData();
|
||||
const { data, dataLoading, dataError } = useDataContext();
|
||||
|
||||
if (dataLoading) return <p>Cargando datos...</p>;
|
||||
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
|
||||
if (!data) return <p>Datos no disponibles.</p>;
|
||||
|
||||
const CardsData = [
|
||||
{ id: 1, title: "Temperatura", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faTemperature0} /> },
|
||||
{ id: 2, title: "Humedad", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faWater} /> },
|
||||
{ id: 3, title: "Nivel de CO", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faCloud} /> },
|
||||
{ id: 4, title: "Actualizado a las", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faClock} /> }
|
||||
{
|
||||
id: 1,
|
||||
title: "Temperatura",
|
||||
content: "N/A",
|
||||
status: "Esperando datos...",
|
||||
titleIcon: '🌡 ',
|
||||
className: "col-12 col-md-6 col-lg-3",
|
||||
link: false,
|
||||
text: true
|
||||
},
|
||||
|
||||
{
|
||||
id: 2,
|
||||
title: "Humedad",
|
||||
content: "N/A",
|
||||
status: "Esperando datos...",
|
||||
titleIcon: '💦 ',
|
||||
className: "col-12 col-md-6 col-lg-3",
|
||||
link: false,
|
||||
text: true
|
||||
},
|
||||
|
||||
{
|
||||
id: 3,
|
||||
title: "Presión",
|
||||
content: "N/A",
|
||||
status: "Esperando datos...",
|
||||
titleIcon: '⏲ ',
|
||||
className: "col-12 col-md-6 col-lg-3",
|
||||
link: false,
|
||||
text: true
|
||||
},
|
||||
|
||||
{
|
||||
id: 4,
|
||||
title: "Nivel de CO",
|
||||
content: "N/A",
|
||||
status: "Esperando datos...",
|
||||
titleIcon: '☁ ',
|
||||
className: "col-12 col-md-6 col-lg-3",
|
||||
link: false,
|
||||
text: true
|
||||
}
|
||||
];
|
||||
|
||||
if (data) {
|
||||
let coData = data[1];
|
||||
let tempData = data[2];
|
||||
|
||||
let lastTime = timestampToTime(coData.airValuesTimestamp);
|
||||
let lastDate = new Date(coData.airValuesTimestamp);
|
||||
if (data) {
|
||||
let coData = data[2];
|
||||
let tempData = data[1];
|
||||
|
||||
CardsData[0].content = tempData.temperature + "°C";
|
||||
CardsData[0].status = "Temperatura actual";
|
||||
CardsData[1].content = tempData.humidity + "%";
|
||||
CardsData[1].status = "Humedad actual";
|
||||
CardsData[2].content = coData.carbonMonoxide + " ppm";
|
||||
CardsData[2].status = "Nivel de CO actual";
|
||||
CardsData[3].content = formatTime(lastTime);
|
||||
CardsData[3].status = "Día " + lastDate.toLocaleDateString();
|
||||
CardsData[3].content = coData.carbonMonoxide + " ppm";
|
||||
CardsData[3].status = "Nivel de CO actual";
|
||||
CardsData[2].content = tempData.pressure + " hPa";
|
||||
CardsData[2].status = "Presión actual";
|
||||
|
||||
}
|
||||
|
||||
@@ -94,6 +106,7 @@ const SummaryCardsContent = () => {
|
||||
}
|
||||
|
||||
SummaryCards.propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useTheme } from "../contexts/ThemeContext.jsx";
|
||||
import "../css/ThemeButton.css";
|
||||
|
||||
/**
|
||||
* ThemeButton.jsx
|
||||
*
|
||||
* Este archivo define el componente ThemeButton, que permite a los usuarios cambiar entre temas claro y oscuro.
|
||||
*
|
||||
* Importaciones:
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
* - "../css/ThemeButton.css": Archivo CSS que contiene los estilos para el botón de cambio de tema.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - ThemeButton: Componente que renderiza un botón para alternar entre temas claro y oscuro.
|
||||
* - Utiliza el hook `useTheme` para acceder al tema actual y la función para cambiarlo.
|
||||
* - El botón muestra un icono de sol (☀️) si el tema actual es oscuro, y un icono de luna (🌙) si el tema actual es claro.
|
||||
*
|
||||
*/
|
||||
|
||||
export default function ThemeButton() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button className="theme-toggle" onClick={toggleTheme}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
94
frontend/src/components/layout/Card.jsx
Normal file
94
frontend/src/components/layout/Card.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "@/css/Card.css";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
|
||||
const Card = ({
|
||||
title,
|
||||
status,
|
||||
children,
|
||||
styleMode,
|
||||
className,
|
||||
titleIcon,
|
||||
style,
|
||||
link,
|
||||
to,
|
||||
text,
|
||||
marquee
|
||||
}) => {
|
||||
const cardRef = useRef(null);
|
||||
const [shortTitle, setShortTitle] = useState(title);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const checkSize = () => {
|
||||
if (cardRef.current) {
|
||||
const width = cardRef.current.offsetWidth;
|
||||
if (width < 300 && title.length > 15) {
|
||||
setShortTitle(title.slice(0, 10) + ".");
|
||||
} else {
|
||||
setShortTitle(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSize();
|
||||
window.addEventListener("resize", checkSize);
|
||||
return () => window.removeEventListener("resize", checkSize);
|
||||
}, [title]);
|
||||
|
||||
const cardContent = (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={`card p-3 w-100 h-100 ${theme} ${className ?? ""}`}
|
||||
style={styleMode === "override" ? style : {}}
|
||||
>
|
||||
<h3 className="text-center">
|
||||
{titleIcon}
|
||||
{shortTitle}
|
||||
</h3>
|
||||
|
||||
|
||||
{marquee && (
|
||||
<div className="contenedor-con-efecto card-content rounded-4 h-100 d-flex align-items-center justify-content-center" style={{ backgroundColor: "black"}}>
|
||||
<marquee scrollamount="30">
|
||||
<p className="card-text text-center m-0">{children}</p>
|
||||
</marquee>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!marquee && (
|
||||
<div className="card-content h-100" >
|
||||
{text ? (
|
||||
<p className="card-text text-center">{children}</p>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status && <span className="status text-center mt-2">{status}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return link && to
|
||||
? <Link to={to} style={{ textDecoration: "none" }}>{cardContent}</Link>
|
||||
: cardContent;
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
styleMode: PropTypes.oneOf(["override", ""]),
|
||||
className: PropTypes.string,
|
||||
titleIcon: PropTypes.node,
|
||||
style: PropTypes.object,
|
||||
link: PropTypes.bool,
|
||||
to: PropTypes.string,
|
||||
text: PropTypes.bool,
|
||||
marquee: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
36
frontend/src/components/layout/CardContainer.jsx
Normal file
36
frontend/src/components/layout/CardContainer.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Card from "./Card.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CardContainer = ({ cards, className }) => {
|
||||
return (
|
||||
<div className={`row justify-content-center g-3 ${className}`}>
|
||||
{cards.map((card, index) => (
|
||||
<div key={index} className={card.className ?? "col-12 col-md-6 col-lg-3"}>
|
||||
<Card {...card}>
|
||||
{card.content}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardContainer.propTypes = {
|
||||
cards: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
styleMode: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
titleIcon: PropTypes.node,
|
||||
link: PropTypes.bool,
|
||||
to: PropTypes.string,
|
||||
text: PropTypes.bool,
|
||||
})
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CardContainer;
|
||||
16
frontend/src/components/layout/ContentWrapper.jsx
Normal file
16
frontend/src/components/layout/ContentWrapper.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ContentWrapper = ({ children, className }) => {
|
||||
return (
|
||||
<div className={`container-xl ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ContentWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ContentWrapper;
|
||||
15
frontend/src/components/layout/CustomContainer.jsx
Normal file
15
frontend/src/components/layout/CustomContainer.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CustomContainer = ({ children }) => {
|
||||
return (
|
||||
<main className="px-4 py-5">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
CustomContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default CustomContainer;
|
||||
12
frontend/src/components/layout/DocsButton.jsx
Normal file
12
frontend/src/components/layout/DocsButton.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import "@/css/DocsButton.css";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const DocsButton = () => {
|
||||
return (
|
||||
<Link to="/docs">
|
||||
<button className="docs-button">📃</button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocsButton;
|
||||
61
frontend/src/components/layout/FloatingMenu.jsx
Normal file
61
frontend/src/components/layout/FloatingMenu.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DocsButton from "./DocsButton";
|
||||
import ThemeButton from "./ThemeButton";
|
||||
import "@/css/FloatingMenu.css";
|
||||
import { faEllipsisVertical } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const FloatingMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const buttonVariants = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
visible: (i) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { delay: i * 0.05, type: "spring", stiffness: 300 }
|
||||
}),
|
||||
exit: { opacity: 0, y: 10, transition: { duration: 0.1 } }
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{ component: <DocsButton />, key: "docs", onClick: () => setOpen(false) },
|
||||
{ component: <ThemeButton />, key: "theme", onClick: () => setOpen(false) }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="floating-menu">
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
className="menu-buttons"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
>
|
||||
{buttons.map((btn, i) => (
|
||||
<motion.div
|
||||
key={btn.key}
|
||||
custom={i}
|
||||
variants={buttonVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={btn.onClick}
|
||||
>
|
||||
{btn.component}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<button className="menu-toggle" onClick={() => setOpen(prev => !prev)}>
|
||||
<FontAwesomeIcon icon={faEllipsisVertical} className="fa-lg" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingMenu;
|
||||
25
frontend/src/components/layout/Header.jsx
Normal file
25
frontend/src/components/layout/Header.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import '@/css/Header.css';
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const Header = ({ subtitle }) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className={`animated-header row justify-content-center text-center mb-4 ${theme}`}>
|
||||
<div className='col-xl-4 col-lg-6 col-8'>
|
||||
<Link to="/" className="text-decoration-none">
|
||||
<img src={`/images/logo-${theme}.png`} className='img-fluid' />
|
||||
</Link>
|
||||
</div>
|
||||
<p className='col-12 text-center my-3'>{subtitle}</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
subtitle: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
14
frontend/src/components/layout/ThemeButton.jsx
Normal file
14
frontend/src/components/layout/ThemeButton.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import "@/css/ThemeButton.css";
|
||||
|
||||
const ThemeButton = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button className="theme-toggle" onClick={toggleTheme}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeButton;
|
||||
41
frontend/src/context/ConfigContext.jsx
Normal file
41
frontend/src/context/ConfigContext.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createContext, useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ConfigContext = createContext();
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [configLoading, setLoading] = useState(true);
|
||||
const [configError, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = import.meta.env.MODE === 'production'
|
||||
? await fetch("/config/settings.prod.json")
|
||||
: await fetch("/config/settings.prod.json");
|
||||
if (!response.ok) throw new Error("Error al cargar settings.*.json");
|
||||
const json = await response.json();
|
||||
setConfig(json);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, configLoading, configError }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export {ConfigContext};
|
||||
23
frontend/src/context/DataContext.jsx
Normal file
23
frontend/src/context/DataContext.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createContext } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useData } from "@/hooks/useData";
|
||||
|
||||
export const DataContext = createContext();
|
||||
|
||||
export const DataProvider = ({ config, children }) => {
|
||||
const data = useData(config);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={data}>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
DataProvider.propTypes = {
|
||||
config: PropTypes.shape({
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
}).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
31
frontend/src/context/ThemeContext.jsx
Normal file
31
frontend/src/context/ThemeContext.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
return (
|
||||
localStorage.getItem("theme") ||
|
||||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
document.body.classList.remove("light", "dark");
|
||||
document.body.classList.add(theme);
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ConfigContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de configuración para la aplicación, permitiendo cargar y manejar la configuración desde un archivo externo.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - ConfigContext: Contexto que almacena la configuración cargada, el estado de carga y cualquier error ocurrido durante la carga de la configuración.
|
||||
* - ConfigProvider: Proveedor de contexto que maneja la carga de la configuración y proporciona el estado de la configuración a los componentes hijos.
|
||||
* - Utiliza `fetch` para cargar la configuración desde un archivo JSON.
|
||||
* - Maneja el estado de carga y errores durante la carga de la configuración.
|
||||
* - useConfig: Hook personalizado para acceder al contexto de configuración.
|
||||
*
|
||||
* PropTypes:
|
||||
* - ConfigProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
*
|
||||
*/
|
||||
|
||||
const ConfigContext = createContext();
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [configLoading, setLoading] = useState(true);
|
||||
const [configError, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/config/settings.json");
|
||||
if (!response.ok) throw new Error("Error al cargar settings.json");
|
||||
const json = await response.json();
|
||||
setConfig(json);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, configLoading, configError }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export const useConfig = () => useContext(ConfigContext);
|
||||
@@ -1,67 +0,0 @@
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* DataContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de datos para la aplicación, permitiendo obtener y manejar datos de una fuente externa.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - DataContext: Contexto que almacena los datos obtenidos, el estado de carga y cualquier error ocurrido durante la obtención de datos.
|
||||
* - DataProvider: Proveedor de contexto que maneja la obtención de datos y proporciona el estado de los datos a los componentes hijos.
|
||||
* - Utiliza `fetch` para obtener datos de una URL construida a partir de la configuración proporcionada.
|
||||
* - Maneja el estado de carga y errores durante la obtención de datos.
|
||||
* - useData: Hook personalizado para acceder al contexto de datos.
|
||||
*
|
||||
* PropTypes:
|
||||
* - DataProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
* - DataProvider también espera una configuración (`config`) que debe incluir `baseUrl` (string) y opcionalmente `params` (objeto).
|
||||
*
|
||||
*/
|
||||
|
||||
const DataContext = createContext();
|
||||
|
||||
export const DataProvider = ({ children, config }) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [dataLoading, setLoading] = useState(true);
|
||||
const [dataError, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams(config.params).toString();
|
||||
const url = `${config.baseUrl}?${queryParams}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Error al obtener datos");
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={{ data, dataLoading, dataError }}>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
DataProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
config: PropTypes.shape({
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const useData = () => useContext(DataContext);
|
||||
@@ -1,55 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ThemeContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de tema para la aplicación, permitiendo cambiar entre temas claro y oscuro.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useEffect, useState: Funciones de React para crear y utilizar contextos, manejar efectos secundarios y estados.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - ThemeContext: Contexto que almacena el tema actual y la función para cambiarlo.
|
||||
* - ThemeProvider: Proveedor de contexto que maneja el estado del tema y proporciona la función para alternar entre temas.
|
||||
* - Utiliza `localStorage` para persistir el tema seleccionado.
|
||||
* - Aplica la clase correspondiente al `body` del documento para reflejar el tema actual.
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
*
|
||||
* PropTypes:
|
||||
* - ThemeProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
*
|
||||
*/
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
return localStorage.getItem("theme") || "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.remove("light", "dark");
|
||||
document.body.classList.add(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user