1
0

5 Commits

156 changed files with 4718 additions and 11252 deletions

373
LICENSE
View File

@@ -1,373 +0,0 @@
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.

182
README.md
View File

@@ -26,9 +26,8 @@ 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;
@@ -40,60 +39,54 @@ CREATE TABLE IF NOT EXISTS groups(
);
CREATE TABLE IF NOT EXISTS devices(
deviceId CHAR(6) PRIMARY KEY NOT NULL,
deviceId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
groupId INT NOT NULL,
deviceName VARCHAR(64) DEFAULT NULL,
FOREIGN KEY (groupId) REFERENCES groups(groupId) ON DELETE CASCADE
FOREIGN KEY (groupId) REFERENCES groups(groupId)
);
CREATE TABLE IF NOT EXISTS sensors(
sensorId INT NOT NULL,
deviceId CHAR(6) NOT NULL,
sensorId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
deviceId INT NOT NULL,
sensorType VARCHAR(64) NOT NULL,
unit VARCHAR(8) NOT NULL,
status INT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (deviceId, sensorId),
FOREIGN KEY (deviceId) REFERENCES devices(deviceId) ON DELETE CASCADE
FOREIGN KEY (deviceId) REFERENCES devices(deviceId)
);
CREATE TABLE IF NOT EXISTS actuators (
actuatorId INT NOT NULL,
deviceId CHAR(6) NOT NULL,
actuatorId INT PRIMARY KEY AUTO_INCREMENT NOT NULL,
deviceId INT NOT NULL,
status INT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (deviceId, actuatorId),
FOREIGN KEY (deviceId) REFERENCES devices(deviceId) ON DELETE CASCADE
FOREIGN KEY (deviceId) REFERENCES devices(deviceId)
);
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 (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
);
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 (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
);
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 (deviceId, sensorId) REFERENCES sensors(deviceId, sensorId) ON DELETE CASCADE
FOREIGN KEY (sensorId) REFERENCES sensors(sensorId)
);
CREATE OR REPLACE VIEW v_sensor_values AS
@@ -105,15 +98,14 @@ 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.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;
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;
CREATE OR REPLACE VIEW v_latest_values AS
@@ -126,15 +118,14 @@ 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.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
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
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));
@@ -147,7 +138,7 @@ SELECT
c.value AS carbonMonoxide,
c.timestamp
FROM sensors s
JOIN co_values c ON s.deviceId = c.deviceId AND s.sensorId = c.sensorId
JOIN co_values c ON s.sensorId = c.sensorId
WHERE s.sensorType = 'CO';
CREATE OR REPLACE VIEW v_gps_by_device AS
@@ -157,7 +148,7 @@ SELECT
g.lon,
g.timestamp
FROM sensors s
JOIN gps_values g ON s.deviceId = g.deviceId AND s.sensorId = g.sensorId
JOIN gps_values g ON s.sensorId = g.sensorId
WHERE s.sensorType = 'GPS';
CREATE OR REPLACE VIEW v_weather_by_device AS
@@ -165,89 +156,100 @@ 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.deviceId = w.deviceId AND s.sensorId = w.sensorId
JOIN weather_values w ON 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
SELECT
d.deviceId,
c.timestamp;
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;
CREATE OR REPLACE VIEW v_sensor_history_by_device AS
SELECT
d.deviceId AS deviceId,
d.deviceName AS deviceName,
SELECT
d.deviceId,
d.deviceName,
w.temperature AS value,
'temperature' AS valueType,
w.timestamp AS timestamp
FROM
dad.devices d
JOIN dad.v_weather_by_device w ON d.deviceId = w.deviceId
w.timestamp
FROM devices d
JOIN v_weather_by_device w ON d.deviceId = w.deviceId
UNION ALL
SELECT
d.deviceId AS deviceId,
d.deviceName AS deviceName,
SELECT
d.deviceId,
d.deviceName,
w.humidity AS value,
'humidity' AS valueType,
w.timestamp AS timestamp
FROM
dad.devices d
JOIN dad.v_weather_by_device w ON d.deviceId = w.deviceId
w.timestamp
FROM devices d
JOIN v_weather_by_device w ON d.deviceId = w.deviceId
UNION ALL
SELECT
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,
SELECT
d.deviceId,
d.deviceName,
c.carbonMonoxide AS value,
'carbonMonoxide' AS valueType,
c.timestamp AS timestamp
FROM
dad.devices d
JOIN dad.v_co_by_device c ON d.deviceId = c.deviceId
ORDER BY
deviceId,
timestamp;
c.timestamp
FROM devices d
JOIN 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 (deviceId, groupId, deviceName) VALUES
('6A6098', 1, 'Dispositivo 1');
INSERT INTO devices (groupId, deviceName) VALUES
(1, 'Dispositivo 1'),
(1, 'Dispositivo 2'),
(1, 'Dispositivo 3');
-- 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 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
-- ACtuadores para el Dispositivo 6A6098
INSERT INTO actuators (actuatorId, deviceId, status, timestamp) VALUES
(1, '6A6098', 1, CURRENT_TIMESTAMP());
-- 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
```

View File

@@ -1,131 +1,98 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.miarma</groupId>
<artifactId>contaminus</artifactId>
<version>1.0.0</version>
<name>ContaminUS</name>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<dependencies>
<!-- Vert.X Core -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<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>
<artifactId>vertx-web</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Web Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X MQTT -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
<version>4.4.2</version>
</dependency>
<!-- Vert.X MariaDB/MySQL Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-jdbc-client</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.12.1</version>
</dependency>
<!-- SLF4J + Logback -->
<dependency>
<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>
<build>
<plugins>
<!-- Maven Shade Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.contaminus.server.MainVerticle</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.miarma</groupId>
<artifactId>contaminus</artifactId>
<version>1.0.0</version>
<name>ContaminUS</name>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<dependencies>
<!-- Vert.X Core -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Web -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Web Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X MQTT -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
<version>4.4.2</version>
</dependency>
<!-- Vert.X MariaDB/MySQL Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-jdbc-client</artifactId>
<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>
<artifactId>gson</artifactId>
<version>2.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.quarkus/quarkus-agroal -->
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
<version>3.1.1.Final</version> <!-- O la versión más reciente -->
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Shade Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.contaminus.server.MainVerticle</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,113 +0,0 @@
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();
}
}

View File

@@ -1,48 +1,55 @@
package net.miarma.contaminus.common;
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 GROUPS = RAW_API_PREFIX + "/groups"; // GET, POST
public static final String GROUP = RAW_API_PREFIX + "/groups/:groupId"; // GET, PUT
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 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 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 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 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.");
}
}
package net.miarma.contaminus.common;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.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 Logger LOGGER = LoggerFactory.getLogger(Constants.APP_NAME);
/* 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 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 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 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 GET_CO_BY_DEVICE_VIEW = RAW_API_PREFIX + "/v_co_by_device";
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";
private Constants() {
throw new AssertionError("Utility class cannot be instantiated.");
}
}

View File

@@ -1,5 +0,0 @@
package net.miarma.contaminus.common;
public interface ValuableEnum {
int getValue();
}

View File

@@ -1,116 +0,0 @@
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");
}
}
}

View File

@@ -1,130 +0,0 @@
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();
}
}

View File

@@ -1,82 +0,0 @@
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");
}
}

View File

@@ -1,128 +0,0 @@
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();
}
}

View File

@@ -1,81 +0,0 @@
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");
}
}

View File

@@ -1,93 +0,0 @@
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();
}
}

View File

@@ -1,130 +0,0 @@
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();
}
}

View File

@@ -1,81 +0,0 @@
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");
}
}

View File

@@ -1,66 +0,0 @@
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");
}
}

View File

@@ -1,66 +0,0 @@
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");
}
}

View File

@@ -1,67 +0,0 @@
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");
}
}

View File

@@ -1,66 +0,0 @@
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");
}
}

View File

@@ -0,0 +1,58 @@
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());
}
});
}
}

View File

@@ -0,0 +1,196 @@
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() + ";";
}
}

View File

@@ -1,92 +1,92 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("actuators")
public class Actuator {
private Integer actuatorId;
private String deviceId;
private Integer status;
private Long timestamp;
public Actuator() {}
public Actuator(Row row) {
this.actuatorId = row.getInteger("actuatorId");
this.deviceId = row.getString("deviceId");
this.status = row.getInteger("status");
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
}
public Actuator(Integer actuatorId, String deviceId, Integer status, Long timestamp) {
super();
this.actuatorId = actuatorId;
this.deviceId = deviceId;
this.status = status;
this.timestamp = timestamp;
}
public Integer getActuatorId() {
return actuatorId;
}
public void setActuatorId(Integer actuatorId) {
this.actuatorId = actuatorId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(actuatorId, deviceId, status, timestamp);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Actuator other = (Actuator) obj;
return actuatorId == other.actuatorId && deviceId == other.deviceId
&& status == other.status && timestamp == other.timestamp;
}
@Override
public String toString() {
return "Actuator [actuatorId=" + actuatorId + ", deviceId=" + deviceId + ", status=" + status + ", timestamp="
+ timestamp + "]";
}
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("actuators")
public class Actuator {
private Integer actuatorId;
private Integer deviceId;
private Integer status;
private Long timestamp;
public Actuator() {}
public Actuator(Row row) {
this.actuatorId = row.getInteger("actuatorId");
this.deviceId = row.getInteger("deviceId");
this.status = row.getInteger("status");
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
}
public Actuator(Integer actuatorId, Integer deviceId, Integer status, Long timestamp) {
super();
this.actuatorId = actuatorId;
this.deviceId = deviceId;
this.status = status;
this.timestamp = timestamp;
}
public Integer getActuatorId() {
return actuatorId;
}
public void setActuatorId(Integer actuatorId) {
this.actuatorId = actuatorId;
}
public Integer getDeviceId() {
return deviceId;
}
public void setDeviceId(Integer deviceId) {
this.deviceId = deviceId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(actuatorId, deviceId, status, timestamp);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Actuator other = (Actuator) obj;
return actuatorId == other.actuatorId && deviceId == other.deviceId
&& status == other.status && timestamp == other.timestamp;
}
@Override
public String toString() {
return "Actuator [actuatorId=" + actuatorId + ", deviceId=" + deviceId + ", status=" + status + ", timestamp="
+ timestamp + "]";
}
}

View File

@@ -1,107 +1,93 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("co_values")
public class COValue {
private Integer valueId;
private String deviceId;
private Integer sensorId;
private Float value;
private Long timestamp;
public 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, String deviceId, Integer sensorId, Float value, Long timestamp) {
super();
this.valueId = valueId;
this.deviceId = deviceId;
this.sensorId = sensorId;
this.value = value;
this.timestamp = timestamp;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
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 getValue() {
return value;
}
public void setValue(Float value) {
this.value = value;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(deviceId, sensorId, timestamp, value, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
COValue other = (COValue) obj;
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 + ", 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());
}
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("co_values")
public class COValue {
private Integer valueId;
private Integer sensorId;
private Float value;
private Long timestamp;
public COValue() {}
public COValue(Row row) {
this.valueId = row.getInteger("valueId");
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) {
super();
this.valueId = valueId;
this.sensorId = sensorId;
this.value = value;
this.timestamp = timestamp;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
public Integer getSensorId() {
return sensorId;
}
public void setSensorId(Integer sensorId) {
this.sensorId = sensorId;
}
public Float getValue() {
return value;
}
public void setValue(Float value) {
this.value = value;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(sensorId, timestamp, value, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
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);
}
@Override
public String toString() {
return "COValue [valueId=" + valueId + ", sensorId=" + sensorId + ", value=" + value + ", timestamp="
+ timestamp + "]";
}
}

View File

@@ -1,65 +1,57 @@
package net.miarma.contaminus.entities;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
@Table("devices")
public class Device {
private String deviceId;
private Integer groupId;
private String deviceName;
private Integer deviceRole;
public Device() {}
public Device(Row row) {
this.deviceId = row.getString("deviceId");
this.groupId = row.getInteger("groupId");
this.deviceName = row.getString("deviceName");
this.deviceRole = row.getInteger("deviceRole");
}
public Device(String deviceId, Integer groupId, String deviceName, Integer deviceRole) {
super();
this.deviceId = deviceId;
this.groupId = groupId;
this.deviceName = deviceName;
this.deviceRole = deviceRole;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public Integer getDeviceRole() {
return deviceRole;
}
public void setDevice(Integer deviceRole) {
this.deviceRole = deviceRole;
}
}
package net.miarma.contaminus.database.entities;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
@Table("devices")
public class Device {
private Integer deviceId;
private Integer groupId;
private String deviceName;
public Device() {}
public Device(Row row) {
this.deviceId = row.getInteger("deviceId");
this.groupId = row.getInteger("groupId");
this.deviceName = row.getString("deviceName");
}
public Device(Integer deviceId, Integer groupId, String deviceName) {
super();
this.deviceId = deviceId;
this.groupId = groupId;
this.deviceName = deviceName;
}
public Integer getDeviceId() {
return deviceId;
}
public void setDeviceId(Integer deviceId) {
this.deviceId = deviceId;
}
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
}

View File

@@ -0,0 +1,67 @@
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
+ "]";
}
}

View File

@@ -0,0 +1,72 @@
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 + "]";
}
}

View File

@@ -1,201 +1,142 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("v_latest_values")
public class ViewLatestValues {
private String deviceId;
private Integer sensorId;
private String sensorType;
private String unit;
private Integer sensorStatus;
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 ViewLatestValues() {}
public ViewLatestValues(Row row) {
this.deviceId = row.getString("deviceId");
this.sensorId = row.getInteger("sensorId");
this.sensorType = row.getString("sensorType");
this.unit = row.getString("unit");
this.sensorStatus = row.getInteger("sensorStatus");
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 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;
this.sensorId = sensorId;
this.sensorType = sensorType;
this.unit = unit;
this.sensorStatus = sensorStatus;
this.sensorTimestamp = sensorTimestamp;
this.temperature = temperature;
this.humidity = humidity;
this.carbonMonoxide = carbonMonoxide;
this.lat = lat;
this.lon = lon;
this.airValuesTimestamp = airValuesTimestamp;
}
public String getDeviceId() {
return deviceId;
}
public Integer getSensorId() {
return sensorId;
}
public String getSensorType() {
return sensorType;
}
public String getUnit() {
return unit;
}
public Integer getSensorStatus() {
return sensorStatus;
}
public Long getSensorTimestamp() {
return sensorTimestamp;
}
public Float getTemperature() {
return temperature;
}
public Float getHumidity() {
return humidity;
}
public Float getPressure() {
return pressure;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Long getAirValuesTimestamp() {
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, pressure, sensorId,
sensorStatus, sensorTimestamp, sensorType, temperature, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
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(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);
}
@Override
public String toString() {
return "ViewLatestValues [deviceId=" + deviceId + ", sensorId=" + sensorId + ", sensorType=" + sensorType
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", sensorTimestamp=" + sensorTimestamp
+ ", temperature=" + temperature + ", humidity=" + humidity + ", pressure=" + pressure
+ ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon=" + lon + ", airValuesTimestamp="
+ airValuesTimestamp + "]";
}
}
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_latest_values")
public class DeviceLatestValuesView {
private Integer deviceId;
private Integer sensorId;
private String sensorType;
private String unit;
private Integer sensorStatus;
private Long sensorTimestamp;
private Float temperature;
private Float humidity;
private Float carbonMonoxide;
private Float lat;
private Float lon;
private Long airValuesTimestamp;
public DeviceLatestValuesView() {}
public DeviceLatestValuesView(Row row) {
this.deviceId = row.getInteger("deviceId");
this.sensorId = row.getInteger("sensorId");
this.sensorType = row.getString("sensorType");
this.unit = row.getString("unit");
this.sensorStatus = row.getInteger("sensorStatus");
this.sensorTimestamp = DateParser.parseDate(row.getLocalDateTime("sensorTimestamp"));
this.temperature = row.getFloat("temperature");
this.humidity = row.getFloat("humidity");
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,
Long airValuesTimestamp) {
super();
this.deviceId = deviceId;
this.sensorId = sensorId;
this.sensorType = sensorType;
this.unit = unit;
this.sensorStatus = sensorStatus;
this.sensorTimestamp = sensorTimestamp;
this.temperature = temperature;
this.humidity = humidity;
this.carbonMonoxide = carbonMonoxide;
this.lat = lat;
this.lon = lon;
this.airValuesTimestamp = airValuesTimestamp;
}
public Integer getDeviceId() {
return deviceId;
}
public Integer getSensorId() {
return sensorId;
}
public String getSensorType() {
return sensorType;
}
public String getUnit() {
return unit;
}
public Integer getSensorStatus() {
return sensorStatus;
}
public Long getSensorTimestamp() {
return sensorTimestamp;
}
public Float getTemperature() {
return temperature;
}
public Float getHumidity() {
return humidity;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Long getAirValuesTimestamp() {
return airValuesTimestamp;
}
@Override
public int hashCode() {
return Objects.hash(airValuesTimestamp, carbonMonoxide, deviceId, humidity, lat, lon, sensorId, sensorStatus,
sensorTimestamp, sensorType, temperature, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DeviceLatestValuesView other = (DeviceLatestValuesView) 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(sensorTimestamp, other.sensorTimestamp)
&& Objects.equals(sensorType, other.sensorType) && Objects.equals(temperature, other.temperature)
&& Objects.equals(unit, other.unit);
}
@Override
public String toString() {
return "DeviceLatestValuesView [deviceId=" + deviceId + ", sensorId=" + sensorId + ", sensorType=" + sensorType
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", sensorTimestamp=" + sensorTimestamp
+ ", temperature=" + temperature + ", humidity=" + humidity + ", carbonMonoxide=" + carbonMonoxide
+ ", lat=" + lat + ", lon=" + lon + ", airValuesTimestamp=" + airValuesTimestamp + "]";
}
}

View File

@@ -1,116 +1,90 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("v_pollution_map")
public class ViewPollutionMap {
private String deviceId;
private String deviceName;
private Float lat;
private Float lon;
private Float carbonMonoxide;
private Long timestamp;
public ViewPollutionMap() {}
public ViewPollutionMap(Row row) {
this.deviceId = row.getString("deviceId");
this.deviceName = row.getString("deviceName");
this.lat = row.getFloat("lat");
this.lon = row.getFloat("lon");
this.carbonMonoxide = row.getFloat("carbonMonoxide");
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
}
public ViewPollutionMap(String deviceId, String deviceName, Float lat, Float lon, Float carbonMonoxide,
Long timestamp) {
super();
this.deviceId = deviceId;
this.deviceName = deviceName;
this.lat = lat;
this.lon = lon;
this.carbonMonoxide = carbonMonoxide;
this.timestamp = timestamp;
}
public String getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Long getTimestamp() {
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);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
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);
}
@Override
public String toString() {
return "DevicePollutionMap [deviceId=" + deviceId + ", deviceName=" + deviceName + ", lat=" + lat + ", lon="
+ lon + ", carbonMonoxide=" + carbonMonoxide + ", timestamp=" + timestamp + "]";
}
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_pollution_map")
public class DevicePollutionMap {
private Integer deviceId;
private String deviceName;
private Float lat;
private Float lon;
private Float carbonMonoxide;
private Long timestamp;
public DevicePollutionMap() {}
public DevicePollutionMap(Row row) {
this.deviceId = row.getInteger("deviceId");
this.deviceName = row.getString("deviceName");
this.lat = row.getFloat("lat");
this.lon = row.getFloat("lon");
this.carbonMonoxide = row.getFloat("carbonMonoxide");
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
}
public DevicePollutionMap(Integer deviceId, String deviceName, Float lat, Float lon, Float carbonMonoxide,
Long timestamp) {
super();
this.deviceId = deviceId;
this.deviceName = deviceName;
this.lat = lat;
this.lon = lon;
this.carbonMonoxide = carbonMonoxide;
this.timestamp = timestamp;
}
public Integer getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Long getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return Objects.hash(carbonMonoxide, deviceId, deviceName, 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;
DevicePollutionMap other = (DevicePollutionMap) 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);
}
@Override
public String toString() {
return "DevicePollutionMap [deviceId=" + deviceId + ", deviceName=" + deviceName + ", lat=" + lat + ", lon="
+ lon + ", carbonMonoxide=" + carbonMonoxide + ", timestamp=" + timestamp + "]";
}
}

View File

@@ -1,104 +1,82 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("v_sensor_history_by_device")
public class ViewSensorHistory {
private String deviceId;
private String deviceName;
private Float value;
private String valueType;
private Long timestamp;
public ViewSensorHistory() {}
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 ViewSensorHistory(String deviceId, String deviceName, Float value, String valueType, Long timestamp) {
super();
this.deviceId = deviceId;
this.deviceName = deviceName;
this.value = value;
this.valueType = valueType;
this.timestamp = timestamp;
}
public String getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public Float getValue() {
return value;
}
public String getValueType() {
return valueType;
}
public Long getTimestamp() {
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);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
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);
}
@Override
public String toString() {
return "DeviceSensorHistory [deviceId=" + deviceId + ", deviceName=" + deviceName + ", value=" + value
+ ", valueType=" + valueType + ", timestamp=" + timestamp + "]";
}
}
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_sensor_history_by_device")
public class DeviceSensorHistory {
private Integer deviceId;
private String deviceName;
private Float value;
private String valueType;
private Long timestamp;
public DeviceSensorHistory() {}
public DeviceSensorHistory(Row row) {
this.deviceId = row.getInteger("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) {
super();
this.deviceId = deviceId;
this.deviceName = deviceName;
this.value = value;
this.valueType = valueType;
this.timestamp = timestamp;
}
public Integer getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public Float getValue() {
return value;
}
public String getValueType() {
return valueType;
}
public Long getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return Objects.hash(deviceId, deviceName, timestamp, value, valueType);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DeviceSensorHistory other = (DeviceSensorHistory) 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);
}
@Override
public String toString() {
return "DeviceSensorHistory [deviceId=" + deviceId + ", deviceName=" + deviceName + ", value=" + value
+ ", valueType=" + valueType + ", timestamp=" + timestamp + "]";
}
}

View File

@@ -1,189 +1,131 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("v_sensor_values")
public class ViewSensorValue {
private Integer sensorId;
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 ViewSensorValue() {}
public ViewSensorValue(Row row) {
this.sensorId = row.getInteger("sensorId");
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 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;
this.sensorType = sensorType;
this.unit = unit;
this.sensorStatus = sensorStatus;
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
this.carbonMonoxide = carbonMonoxide;
this.lat = lat;
this.lon = lon;
this.timestamp = timestamp;
}
public Integer getSensorId() {
return sensorId;
}
public String getDeviceId() {
return deviceId;
}
public String getSensorType() {
return sensorType;
}
public String getUnit() {
return unit;
}
public Integer getSensorStatus() {
return sensorStatus;
}
public Float getTemperature() {
return temperature;
}
public Float getHumidity() {
return humidity;
}
public Float getPressure() {
return pressure;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Long getTimestamp() {
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, pressure, sensorId, sensorStatus, sensorType,
temperature, timestamp, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
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(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 "ViewSensorValue [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", temperature=" + temperature + ", humidity="
+ humidity + ", pressure=" + pressure + ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon="
+ lon + ", timestamp=" + timestamp + "]";
}
}
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_sensor_values")
public class DeviceSensorValue {
private Integer sensorId;
private Integer deviceId;
private String sensorType;
private String unit;
private Integer sensorStatus;
private Float temperature;
private Float humidity;
private Float carbonMonoxide;
private Float lat;
private Float lon;
private Long timestamp;
public DeviceSensorValue() {}
public DeviceSensorValue(Row row) {
this.sensorId = row.getInteger("sensorId");
this.deviceId = row.getInteger("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.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) {
super();
this.sensorId = sensorId;
this.deviceId = deviceId;
this.sensorType = sensorType;
this.unit = unit;
this.sensorStatus = sensorStatus;
this.temperature = temperature;
this.humidity = humidity;
this.carbonMonoxide = carbonMonoxide;
this.lat = lat;
this.lon = lon;
this.timestamp = timestamp;
}
public Integer getSensorId() {
return sensorId;
}
public Integer getDeviceId() {
return deviceId;
}
public String getSensorType() {
return sensorType;
}
public String getUnit() {
return unit;
}
public Integer getSensorStatus() {
return sensorStatus;
}
public Float getTemperature() {
return temperature;
}
public Float getHumidity() {
return humidity;
}
public Float getCarbonMonoxide() {
return carbonMonoxide;
}
public Float getLat() {
return lat;
}
public Float getLon() {
return lon;
}
public Long getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return Objects.hash(carbonMonoxide, deviceId, humidity, lat, lon, sensorId, sensorStatus, sensorType,
temperature, timestamp, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DeviceSensorValue other = (DeviceSensorValue) 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);
}
@Override
public String toString() {
return "DeviceSensorValue [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType
+ ", unit=" + unit + ", sensorStatus=" + sensorStatus + ", temperature=" + temperature + ", humidity="
+ humidity + ", carbonMonoxide=" + carbonMonoxide + ", lat=" + lat + ", lon=" + lon + ", timestamp="
+ timestamp + "]";
}
}

View File

@@ -0,0 +1,74 @@
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 + "]";
}
}

View File

@@ -1,118 +1,104 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("gps_values")
public class GpsValue {
private Integer valueId;
private String deviceId;
private Integer sensorId;
private Float lat;
private Float lon;
private Long timestamp;
public 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, 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;
this.timestamp = timestamp;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
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 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(deviceId, lat, lon, sensorId, timestamp, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
GpsValue other = (GpsValue) obj;
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 + ", 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());
}
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("gps_values")
public class GpsValue {
private Integer valueId;
private Integer sensorId;
private Float lat;
private Float lon;
private Long timestamp;
public GpsValue() {}
public GpsValue(Row row) {
this.valueId = row.getInteger("valueId");
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) {
super();
this.valueId = valueId;
this.sensorId = sensorId;
this.lat = lat;
this.lon = lon;
this.timestamp = timestamp;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
public Integer getSensorId() {
return sensorId;
}
public void setSensorId(Integer sensorId) {
this.sensorId = sensorId;
}
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(lat, lon, sensorId, timestamp, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
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);
}
@Override
public String toString() {
return "GpsValue [valueId=" + valueId + ", sensorId=" + sensorId + ", lat=" + lat + ", lon=" + lon
+ ", timestamp=" + timestamp + "]";
}
}

View File

@@ -1,66 +1,66 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
@Table("groups")
public class Group {
private Integer groupId;
private String groupName;
public Group() {}
public Group(Row row) {
this.groupId = row.getInteger("groupId");
this.groupName = row.getString("groupName");
}
public Group(Integer groupId, String groupName) {
super();
this.groupId = groupId;
this.groupName = groupName;
}
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public int hashCode() {
return Objects.hash(groupId, groupName);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Group other = (Group) obj;
return Objects.equals(groupId, other.groupId) && Objects.equals(groupName, other.groupName);
}
@Override
public String toString() {
return "Group [groupId=" + groupId + ", groupName=" + groupName + "]";
}
package net.miarma.contaminus.database.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
@Table("groups")
public class Group {
private Integer groupId;
private String groupName;
public Group() {}
public Group(Row row) {
this.groupId = row.getInteger("groupId");
this.groupName = row.getString("groupName");
}
public Group(Integer groupId, String groupName) {
super();
this.groupId = groupId;
this.groupName = groupName;
}
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public int hashCode() {
return Objects.hash(groupId, groupName);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Group other = (Group) obj;
return Objects.equals(groupId, other.groupId) && Objects.equals(groupName, other.groupName);
}
@Override
public String toString() {
return "Group [groupId=" + groupId + ", groupName=" + groupName + "]";
}
}

View File

@@ -1,114 +1,114 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("sensors")
public class Sensor {
private Integer sensorId;
private String deviceId;
private String sensorType;
private String unit;
private Integer status;
private Long timestamp;
public Sensor() {}
public Sensor(Row row) {
this.sensorId = row.getInteger("sensorId");
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, String deviceId, String sensorType, String unit, Integer status, Long timestamp) {
super();
this.sensorId = sensorId;
this.deviceId = deviceId;
this.sensorType = sensorType;
this.unit = unit;
this.status = status;
this.timestamp = timestamp;
}
public Integer getSensorId() {
return sensorId;
}
public void setSensorId(Integer sensorId) {
this.sensorId = sensorId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getSensorType() {
return sensorType;
}
public void setSensorType(String sensorType) {
this.sensorType = sensorType;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(deviceId, sensorId, sensorType, status, timestamp, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Sensor other = (Sensor) obj;
return Objects.equals(deviceId, other.deviceId) && Objects.equals(sensorId, other.sensorId)
&& Objects.equals(sensorType, other.sensorType) && Objects.equals(status, other.status)
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(unit, other.unit);
}
@Override
public String toString() {
return "Sensor [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType + ", unit="
+ unit + ", status=" + status + ", timestamp=" + timestamp + "]";
}
}
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("sensors")
public class Sensor {
private Integer sensorId;
private Integer deviceId;
private String sensorType;
private String unit;
private Integer status;
private Long timestamp;
public Sensor() {}
public Sensor(Row row) {
this.sensorId = row.getInteger("sensorId");
this.deviceId = row.getInteger("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) {
super();
this.sensorId = sensorId;
this.deviceId = deviceId;
this.sensorType = sensorType;
this.unit = unit;
this.status = status;
this.timestamp = timestamp;
}
public Integer getSensorId() {
return sensorId;
}
public void setSensorId(Integer sensorId) {
this.sensorId = sensorId;
}
public Integer getDeviceId() {
return deviceId;
}
public void setDeviceId(Integer deviceId) {
this.deviceId = deviceId;
}
public String getSensorType() {
return sensorType;
}
public void setSensorType(String sensorType) {
this.sensorType = sensorType;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(deviceId, sensorId, sensorType, status, timestamp, unit);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Sensor other = (Sensor) obj;
return Objects.equals(deviceId, other.deviceId) && Objects.equals(sensorId, other.sensorId)
&& Objects.equals(sensorType, other.sensorType) && Objects.equals(status, other.status)
&& Objects.equals(timestamp, other.timestamp) && Objects.equals(unit, other.unit);
}
@Override
public String toString() {
return "Sensor [sensorId=" + sensorId + ", deviceId=" + deviceId + ", sensorType=" + sensorType + ", unit="
+ unit + ", status=" + status + ", timestamp=" + timestamp + "]";
}
}

View File

@@ -1,131 +1,104 @@
package net.miarma.contaminus.entities;
import java.util.Objects;
import io.vertx.sqlclient.Row;
import net.miarma.contaminus.common.Table;
import net.miarma.contaminus.util.DateParser;
@Table("weather_values")
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, 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;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
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 void setPressure(Float pressure) {
this.pressure = pressure;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(deviceId, humidity, pressure, sensorId, temperature, timestamp, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
WeatherValue other = (WeatherValue) obj;
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 + ", 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());
}
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("weather_values")
public class WeatherValue {
private Integer valueId;
private Integer sensorId;
private Float temperature;
private Float humidity;
private Long timestamp;
public WeatherValue() {}
public WeatherValue(Row row) {
this.valueId = row.getInteger("valueId");
this.sensorId = row.getInteger("sensorId");
this.temperature = row.getFloat("temperature");
this.humidity = row.getFloat("humidity");
this.timestamp = DateParser.parseDate(row.getLocalDateTime("timestamp"));
}
public WeatherValue(Integer valueId, Integer sensorId, Float temperature, Float humidity, Long timestamp) {
super();
this.valueId = valueId;
this.sensorId = sensorId;
this.temperature = temperature;
this.humidity = humidity;
this.timestamp = timestamp;
}
public Integer getValueId() {
return valueId;
}
public void setValueId(Integer valueId) {
this.valueId = valueId;
}
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 Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
@Override
public int hashCode() {
return Objects.hash(humidity, sensorId, temperature, timestamp, valueId);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
WeatherValue other = (WeatherValue) obj;
return Objects.equals(humidity, other.humidity) && 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 + "]";
}
}

View File

@@ -1,13 +0,0 @@
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);
}

View File

@@ -1,82 +0,0 @@
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());
}
});
}
}

View File

@@ -1,21 +0,0 @@
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);
}
}

View File

@@ -1,357 +0,0 @@
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() + ";";
}
}

View File

@@ -1,130 +0,0 @@
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 + "]";
}
}

View File

@@ -0,0 +1,480 @@
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);
});
}
}

View File

@@ -0,0 +1,225 @@
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);
}
}

View File

@@ -1,96 +1,107 @@
package net.miarma.contaminus.verticles;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Launcher;
import io.vertx.core.Promise;
import io.vertx.core.ThreadingModel;
import net.miarma.contaminus.common.ConfigManager;
import net.miarma.contaminus.common.Constants;
public class MainVerticle extends AbstractVerticle {
private ConfigManager configManager;
public static void main(String[] args) {
Launcher.executeCommand("run", MainVerticle.class.getName());
}
private void init() {
this.configManager = ConfigManager.getInstance();
initializeDirectories();
copyDefaultConfig();
}
private void initializeDirectories() {
File baseDir = new File(this.configManager.getBaseDir());
if (!baseDir.exists()) {
baseDir.mkdirs();
}
}
private void copyDefaultConfig() {
File configFile = new File(configManager.getConfigFile().getAbsolutePath());
if (!configFile.exists()) {
try (InputStream in = MainVerticle.class.getClassLoader().getResourceAsStream("default.properties")) {
if (in != null) {
Files.copy(in, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} else {
Constants.LOGGER.error("🔴 Default config file not found in resources");
}
} catch (IOException e) {
Constants.LOGGER.error("🔴 Failed to copy default config file", e);
}
}
}
@Override
public void start(Promise<Void> startPromise) {
try {
init();
deployVerticles(startPromise);
} catch (Exception e) {
Constants.LOGGER.error("🔴 Error starting the application: " + e);
startPromise.fail(e);
}
}
private void deployVerticles(Promise<Void> startPromise) {
final DeploymentOptions options = new DeploymentOptions();
options.setThreadingModel(ThreadingModel.WORKER);
vertx.deployVerticle(new DataLayerAPIVerticle(), options, result -> {
if (result.succeeded()) {
Constants.LOGGER.info("🟢 DatabaseVerticle desplegado");
Constants.LOGGER.info("\t🔗 API URL: " + configManager.getHost()
+ ":" + configManager.getDataApiPort());
} else {
Constants.LOGGER.error("🔴 Error deploying DataLayerAPIVerticle: " + result.cause());
}
});
vertx.deployVerticle(new LogicLayerAPIVerticle(), options, result -> {
if (result.succeeded()) {
Constants.LOGGER.info("🟢 LogicLayerAPIVerticle desplegado");
Constants.LOGGER.info("\t🔗 API URL: " + configManager.getHost()
+ ":" + configManager.getLogicApiPort());
} else {
Constants.LOGGER.error("🔴 Error deploying LogicLayerAPIVerticle: " + result.cause());
}
});
startPromise.complete();
}
@Override
public void stop(Promise<Void> stopPromise) throws Exception {
getVertx().deploymentIDs()
.forEach(v -> getVertx().undeploy(v));
}
}
package net.miarma.contaminus.server;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Launcher;
import io.vertx.core.Promise;
import io.vertx.core.ThreadingModel;
import net.miarma.contaminus.common.ConfigManager;
import net.miarma.contaminus.common.Constants;
public class MainVerticle extends AbstractVerticle {
private ConfigManager configManager;
public static void main(String[] args) {
Launcher.executeCommand("run", MainVerticle.class.getName());
}
private void init() {
this.configManager = ConfigManager.getInstance();
initializeDirectories();
copyDefaultConfig();
}
private void initializeDirectories() {
File baseDir = new File(this.configManager.getBaseDir());
if (!baseDir.exists()) {
baseDir.mkdirs();
}
}
private void copyDefaultConfig() {
File configFile = new File(configManager.getConfigFile().getAbsolutePath());
if (!configFile.exists()) {
try (InputStream in = MainVerticle.class.getClassLoader().getResourceAsStream("default.properties")) {
if (in != null) {
Files.copy(in, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} else {
Constants.LOGGER.error("🔴 Default config file not found in resources");
}
} catch (IOException e) {
Constants.LOGGER.error("🔴 Failed to copy default config file", e);
}
}
}
@Override
public void start(Promise<Void> startPromise) {
try {
System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
init();
deployVerticles(startPromise);
} catch (Exception e) {
Constants.LOGGER.error("🔴 Error starting the application: " + e);
startPromise.fail(e);
}
}
private void deployVerticles(Promise<Void> startPromise) {
final DeploymentOptions options = new DeploymentOptions();
options.setThreadingModel(ThreadingModel.WORKER);
vertx.deployVerticle(new DataLayerAPIVerticle(), options, result -> {
if (result.succeeded()) {
Constants.LOGGER.info("🟢 DatabaseVerticle desplegado");
Constants.LOGGER.info("\t🔗 API URL: " + configManager.getHost()
+ ":" + configManager.getDataApiPort());
} else {
Constants.LOGGER.error("🔴 Error deploying DataLayerAPIVerticle: " + result.cause());
}
});
vertx.deployVerticle(new LogicLayerAPIVerticle(), options, result -> {
if (result.succeeded()) {
Constants.LOGGER.info("🟢 LogicLayerAPIVerticle desplegado");
Constants.LOGGER.info("\t🔗 API URL: " + configManager.getHost()
+ ":" + configManager.getLogicApiPort());
} else {
Constants.LOGGER.error("🔴 Error deploying LogicLayerAPIVerticle: " + result.cause());
}
});
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();
}
@Override
public void stop(Promise<Void> stopPromise) throws Exception {
getVertx().deploymentIDs()
.forEach(v -> getVertx().undeploy(v));
}
}

View File

@@ -0,0 +1,7 @@
package net.miarma.contaminus.server;
import io.vertx.core.AbstractVerticle;
public class MqttVerticle extends AbstractVerticle {
}

View File

@@ -0,0 +1,50 @@
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();
}
}

View File

@@ -4,54 +4,136 @@ import java.util.Map;
import com.google.gson.Gson;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.WebClient;
public class RestClientUtil {
public WebClient client;
private Gson gson;
public RestClientUtil(WebClient client) {
gson = new Gson();
this.client = client;
}
private final WebClient client;
private final Gson 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 RestClientUtil(WebClient client) {
this.client = client;
this.gson = new Gson();
}
}
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));
}
/**
* 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);
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);
params.forEach((key, value) -> {
httpRequest.addQueryParam(key, value);
});
return httpRequest.send()
.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> 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));
}
}
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());
}
});
}
public Future<String> deleteRequest(int port, String host, String resource) {
return client.deleteAbs(host + ":" + port + "/" + resource)
.send()
.map(response -> response.bodyAsString());
}
/**
* 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());
}
});
}
}

View File

@@ -1,556 +0,0 @@
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);
});
}
}

View File

@@ -1,322 +0,0 @@
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;
}
}

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,15 +1,14 @@
# DB Configuration
db.protocol=jdbc:mariadb
db.host=localhost
db.port=3306
db.name=dad
db.user=root
db.password=root
dp.poolSize=5
# HTTP Server Configuration
inet.host=localhost
mqtt.host=localhost
webserver.port=8080
data-api.port=8081
# DB Configuration
db.protocol=jdbc:mariadb
db.host=localhost
db.port=3306
db.name=dad
db.user=root
db.pwd=root
dp.poolSize=5
# HTTP Server Configuration
inet.host=localhost
webserver.port=8080
data-api.port=8081
logic-api.port=8082

View File

@@ -1,20 +0,0 @@
<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>

View File

@@ -1,325 +0,0 @@
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

View File

@@ -1,70 +0,0 @@
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'")

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,19 +15,14 @@
"@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",
"swagger-ui-react": "^5.22.0"
"react-router-dom": "^7.3.0"
},
"devDependencies": {
"@eslint/js": "^9.19.0",

View File

@@ -1 +0,0 @@
dh=39376f6548b4449fc0faf969d98f6f7a10af9e7e

View File

@@ -1,481 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,77 +0,0 @@
{
"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 } }
}
}
}
}

View File

@@ -0,0 +1,98 @@
{
"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
}
}
}
}
}
}

View File

@@ -1,77 +0,0 @@
{
"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 } }
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +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 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;

View File

@@ -1,14 +0,0 @@
import axios from "axios";
const createAxiosInstance = (baseURL, token) => {
const instance = axios.create({
baseURL,
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
});
return instance;
};
export default createAxiosInstance;

View File

@@ -1,74 +0,0 @@
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;

View File

@@ -0,0 +1,70 @@
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;

View File

@@ -0,0 +1,84 @@
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;

View File

@@ -0,0 +1,46 @@
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;

View File

@@ -0,0 +1,41 @@
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;

View File

@@ -1,34 +1,56 @@
import { Line } from "react-chartjs-2";
import { Chart as ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Filler } from "chart.js";
import CardContainer from "./layout/CardContainer";
import CardContainer from "./CardContainer";
import "../css/HistoryCharts.css";
import PropTypes from "prop-types";
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";
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).
*
*/
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler);
const HistoryCharts = ({ groupId, deviceId }) => {
const HistoryCharts = () => {
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.LOGIC_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_HISTORY;
const endp = ENDPOINT
.replace(':groupId', groupId)
.replace(':deviceId', deviceId);
const ENDPOINT = config.appConfig.endpoints.sensors;
const reqConfig = {
baseUrl: `${BASE}${endp}`,
params: {}
};
baseUrl: `${BASE}/${ENDPOINT}`,
params: {}
}
return (
<DataProvider config={reqConfig}>
@@ -39,102 +61,62 @@ const HistoryCharts = ({ groupId, deviceId }) => {
const HistoryChartsContent = () => {
const { config } = useConfig();
const { data, loading, error } = useDataContext();
const { data, loading } = useData();
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 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;
const temperatureData = [];
const humidityData = [];
const pollutionLevels = [];
data?.forEach(sensor => {
if (
sensor.value != null &&
grouped[sensor.valueType] &&
isRecent(sensor.timestamp)
) {
grouped[sensor.valueType].push({
timestamp: sensor.timestamp * 1000,
value: sensor.value
});
}
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);
}
}
});
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: 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)" }
];
{ 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)" }
];
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}
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>
</>
<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=""
/>
);
};
HistoryCharts.propTypes = {
groupId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired
};
HistoryChartsContent.propTypes = {
options: PropTypes.object,
timeLabels: PropTypes.array,

View File

@@ -1,10 +0,0 @@
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;

View File

@@ -0,0 +1,35 @@
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,
};

View File

@@ -1,32 +1,78 @@
import { MapContainer, TileLayer, Circle, Popup } from 'react-leaflet';
import PropTypes from 'prop-types';
import { useConfig } from '@/hooks/useConfig.js';
import { useConfig } from '../contexts/ConfigContext.jsx';
import { DataProvider } from '@/context/DataContext.jsx';
import { useDataContext } from '@/hooks/useDataContext';
import { DataProvider } from '../contexts/DataContext.jsx';
import { useData } from '../contexts/DataContext.jsx';
import L from "leaflet";
import "leaflet.heat";
/**
* 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 { useEffect, useState, useRef } from 'react';
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;
const PollutionMap = ({ groupId, deviceId }) => {
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/</Popup>
</Circle>
</div>
);
});
};
const PollutionMap = ({ deviceId }) => {
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.LOGIC_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_POLLUTION_MAP;
const endp = ENDPOINT
.replace(':groupId', groupId)
.replace(':deviceId', deviceId);
let endp = ENDPOINT.replace('{0}', deviceId);
const reqConfig = {
baseUrl: `${BASE}${endp}`,
params: {}
};
baseUrl: `${BASE}/${endp}`,
params: {}
}
return (
<DataProvider config={reqConfig}>
@@ -37,167 +83,44 @@ const PollutionMap = ({ groupId, deviceId }) => {
const PollutionMapContent = () => {
const { config, configLoading, configError } = useConfig();
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:
'&copy; <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);
}
};
const { data, dataLoading, dataError } = useData();
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: {dataError}</p>;
if (dataError) return <p>Error al cargar datos: {configError}</p>;
if (!data) return <p>Datos no disponibles.</p>;
return (
<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={{
const SEVILLA = config?.userConfig.city;
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>
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='&copy; 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>
);
}
const mapStyles = {
height: '500px',
width: '100%',
borderRadius: '20px'
};
PollutionMap.propTypes = {
groupId: PropTypes.number.isRequired,
deviceId: PropTypes.number.isRequired
};

View File

@@ -0,0 +1,103 @@
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;

View File

@@ -1,113 +1,100 @@
import PropTypes from 'prop-types';
import CardContainer from './layout/CardContainer';
import { DataProvider } from '@/context/DataContext';
import { useDataContext } from '@/hooks/useDataContext';
import { useConfig } from '@/hooks/useConfig.js';
const SummaryCards = ({ groupId, deviceId }) => {
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.LOGIC_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_LATEST_VALUES;
const endp = ENDPOINT
.replace(':groupId', groupId)
.replace(':deviceId', deviceId);
const reqConfig = {
baseUrl: `${BASE}${endp}`,
params: {}
};
return (
<DataProvider config={reqConfig}>
<SummaryCardsContent />
</DataProvider>
);
};
const SummaryCardsContent = () => {
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: '🌡 ',
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[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[3].content = coData.carbonMonoxide + " ppm";
CardsData[3].status = "Nivel de CO actual";
CardsData[2].content = tempData.pressure + " hPa";
CardsData[2].status = "Presión actual";
}
return (
<CardContainer cards={CardsData} />
);
}
SummaryCards.propTypes = {
groupId: PropTypes.string.isRequired,
deviceId: PropTypes.number.isRequired
};
import PropTypes from 'prop-types';
import CardContainer from './CardContainer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCloud, faClock, faTemperature0, faWater } from '@fortawesome/free-solid-svg-icons';
import { DataProvider } from '../contexts/DataContext';
import { useData } from '../contexts/DataContext';
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 { 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.LOGIC_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_LATEST_VALUES;
const endp = ENDPOINT.replace('{0}', deviceId);
const reqConfig = {
baseUrl: `${BASE}/${endp}`,
params: {}
}
return (
<DataProvider config={reqConfig}>
<SummaryCardsContent deviceId={deviceId} />
</DataProvider>
);
}
const SummaryCardsContent = () => {
const { data, dataLoading, dataError } = useData();
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} /> }
];
if (data) {
let coData = data[1];
let tempData = data[2];
let lastTime = timestampToTime(coData.airValuesTimestamp);
let lastDate = new Date(coData.airValuesTimestamp);
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();
}
return (
<CardContainer cards={CardsData} />
);
}
SummaryCards.propTypes = {
deviceId: PropTypes.number.isRequired
};
export default SummaryCards;

View File

@@ -0,0 +1,28 @@
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>
);
}

View File

@@ -1,94 +0,0 @@
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;

View File

@@ -1,36 +0,0 @@
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;

View File

@@ -1,16 +0,0 @@
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;

View File

@@ -1,15 +0,0 @@
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;

View File

@@ -1,12 +0,0 @@
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;

View File

@@ -1,61 +0,0 @@
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;

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,14 +0,0 @@
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;

View File

@@ -1,41 +0,0 @@
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};

View File

@@ -1,23 +0,0 @@
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,
};

View File

@@ -1,31 +0,0 @@
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>
);
};

View File

@@ -0,0 +1,60 @@
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);

View File

@@ -0,0 +1,67 @@
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);

View File

@@ -0,0 +1,55 @@
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