From 671304179bd01f56191ec4cdff29780280c4aff5 Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 19:44:56 +0200 Subject: [PATCH 01/10] add tables --- ...29_cc68eca3644d_add_timeseries_metadata.py | 67 +++++++++++ lib/py_carlos_database/carlos/database/orm.py | 105 +++++++++++++++++- .../edge/interface/device/driver_config.py | 2 + 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 lib/py_carlos_database/carlos/database/_migrations/versions/2024-04-29_cc68eca3644d_add_timeseries_metadata.py diff --git a/lib/py_carlos_database/carlos/database/_migrations/versions/2024-04-29_cc68eca3644d_add_timeseries_metadata.py b/lib/py_carlos_database/carlos/database/_migrations/versions/2024-04-29_cc68eca3644d_add_timeseries_metadata.py new file mode 100644 index 00000000..9058d09a --- /dev/null +++ b/lib/py_carlos_database/carlos/database/_migrations/versions/2024-04-29_cc68eca3644d_add_timeseries_metadata.py @@ -0,0 +1,67 @@ +"""add timeseries metadata + +Revision ID: cc68eca3644d +Revises: a035faa3c7d7 +Create Date: 2024-04-29 19:17:40.883900 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "cc68eca3644d" +down_revision = "a035faa3c7d7" +branch_labels = None +depends_on = None + + +def upgrade(): + driver_ddl = """ + CREATE TABLE carlos.device_driver ( + device_id UUID NOT NULL + REFERENCES carlos.device(device_id) ON DELETE CASCADE, + driver_identifier VARCHAR(64) NOT NULL, + direction VARCHAR(32) NOT NULL, + driver_module VARCHAR(255) NOT NULL, + display_name VARCHAR(255) NOT NULL, + is_visible_on_dashboard BOOLEAN NOT NULL, + PRIMARY KEY (device_id, driver_identifier) + ); + COMMENT ON TABLE carlos.device_driver IS 'Contains the metadata for a given driver of a device.'; + COMMENT ON COLUMN carlos.device_driver.device_id IS 'The device the driver belongs to.'; + COMMENT ON COLUMN carlos.device_driver.driver_identifier IS 'The unique identifier of the driver in the context of the device.'; + COMMENT ON COLUMN carlos.device_driver.direction IS 'The direction of the IO.'; + COMMENT ON COLUMN carlos.device_driver.driver_module IS 'The module that implements the IO driver.'; + COMMENT ON COLUMN carlos.device_driver.display_name IS 'The name of the driver that is displayed in the UI.'; + COMMENT ON COLUMN carlos.device_driver.is_visible_on_dashboard IS 'Whether the driver is visible on the dashboard.'; + """ + op.execute(driver_ddl) + + signal_ddl = """ + CREATE TABLE carlos.device_signal ( + timeseries_id SERIAL PRIMARY KEY, + device_id UUID NOT NULL, + driver_identifier VARCHAR(64) NOT NULL, + signal_identifier VARCHAR(64) NOT NULL, + display_name VARCHAR(255) NOT NULL, + unit_of_measurement SMALLINT NOT NULL, + is_visible_on_dashboard BOOLEAN NOT NULL, + FOREIGN KEY (device_id, driver_identifier) + REFERENCES carlos.device_driver(device_id, driver_identifier) ON DELETE CASCADE, + UNIQUE (device_id, driver_identifier, signal_identifier) + ); + COMMENT ON TABLE carlos.device_signal IS 'Contains the metadata for a given signal of a driver.'; + COMMENT ON COLUMN carlos.device_signal.timeseries_id IS 'The unique identifier of the signal.'; + COMMENT ON COLUMN carlos.device_signal.device_id IS 'The device the signal belongs to.'; + COMMENT ON COLUMN carlos.device_signal.driver_identifier IS 'The driver the signal belongs to.'; + COMMENT ON COLUMN carlos.device_signal.signal_identifier IS 'The unique identifier of the signal in the context of the driver.'; + COMMENT ON COLUMN carlos.device_signal.display_name IS 'The name of the signal that is displayed in the UI.'; + COMMENT ON COLUMN carlos.device_signal.unit_of_measurement IS 'The unit of measurement of the driver.'; + COMMENT ON COLUMN carlos.device_signal.is_visible_on_dashboard IS 'Whether the signal is visible on the dashboard.'; + """ + op.execute(signal_ddl) + + +def downgrade(): + op.execute("DROP TABLE carlos.device_signal;") + op.execute("DROP TABLE carlos.device_driver;") diff --git a/lib/py_carlos_database/carlos/database/orm.py b/lib/py_carlos_database/carlos/database/orm.py index 23da3d63..079b9703 100644 --- a/lib/py_carlos_database/carlos/database/orm.py +++ b/lib/py_carlos_database/carlos/database/orm.py @@ -9,7 +9,17 @@ from enum import Enum from uuid import UUID -from sqlalchemy import TEXT, TIMESTAMP, VARCHAR, text +from sqlalchemy import ( + BOOLEAN, + INTEGER, + SMALLINT, + TEXT, + TIMESTAMP, + VARCHAR, + ForeignKey, + ForeignKeyConstraint, + text, +) from sqlalchemy.dialects.postgresql import UUID as SQLUUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -70,3 +80,96 @@ class CarlosDeviceOrm(CarlosModelBase): nullable=True, comment="The date and time when the server last received data from the device.", ) + + +class CarlosDeviceDriverOrm(CarlosModelBase): + """Contains the metadata for a given driver of a device.""" + + __tablename__ = "device_driver" + __table_args__ = { + "schema": CarlosDatabaseSchema.CARLOS.value, + "comment": _clean_doc(__doc__), + } + + device_id: Mapped[UUID] = mapped_column( + SQLUUID(as_uuid=True), + ForeignKey(CarlosDeviceOrm.device_id, ondelete="CASCADE"), + primary_key=True, + comment="The device the driver belongs to.", + ) + driver_identifier: Mapped[str] = mapped_column( + VARCHAR(64), + primary_key=True, + comment="The unique identifier of the driver in the context of the device.", + ) + direction: Mapped[str] = mapped_column( + VARCHAR(32), + nullable=False, + comment="The direction of the IO.", + ) + driver_module: Mapped[str] = mapped_column( + VARCHAR(255), + nullable=False, + comment="The module that implements the IO driver.", + ) + display_name: Mapped[str] = mapped_column( + VARCHAR(255), + nullable=False, + comment="The name of the driver that is displayed in the UI.", + ) + is_visible_on_dashboard: Mapped[bool] = mapped_column( + BOOLEAN, + nullable=False, + comment="Whether the driver is visible on the dashboard.", + ) + + +class CarlosDeviceSignalOrm(CarlosModelBase): + """Contains the metadata for a given signal of a driver.""" + + __tablename__ = "device_signal" + __table_args__ = ( + ForeignKeyConstraint( + ["device_id", "driver_identifier"], + [CarlosDeviceDriverOrm.device_id, CarlosDeviceDriverOrm.driver_identifier], + ondelete="CASCADE", + ), + { + "schema": CarlosDatabaseSchema.CARLOS.value, + "comment": _clean_doc(__doc__), + }, + ) + + timeseries_id: Mapped[int] = mapped_column( + INTEGER, + primary_key=True, + autoincrement=True, + comment="The unique identifier of the signal.", + ) + device_id: Mapped[UUID] = mapped_column( + SQLUUID(as_uuid=True), + comment="The device the signal belongs to.", + ) + driver_identifier: Mapped[str] = mapped_column( + VARCHAR(64), + comment="The driver the signal belongs to.", + ) + signal_identifier: Mapped[str] = mapped_column( + VARCHAR(64), + comment="The unique identifier of the signal in the context of the driver.", + ) + display_name: Mapped[str] = mapped_column( + VARCHAR(255), + nullable=False, + comment="The name of the signal that is displayed in the UI.", + ) + unit_of_measurement: Mapped[int] = mapped_column( + SMALLINT, + nullable=False, + comment="The unit of measurement of the driver.", + ) + is_visible_on_dashboard: Mapped[bool] = mapped_column( + BOOLEAN, + nullable=False, + comment="Whether the signal is visible on the dashboard.", + ) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py index 7fa9ee97..f34d6c34 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py @@ -31,6 +31,7 @@ class _DriverConfigMixin(CarlosSchema): identifier: str = Field( ..., + min_length=1, max_length=DRIVER_IDENTIFIER_LENGTH, description="A unique identifier for the driver_module configuration. " "It is used to allow changing addresses, pins if required later.", @@ -38,6 +39,7 @@ class _DriverConfigMixin(CarlosSchema): driver_module: str = Field( ..., + max_length=255, description="Refers to the module name that implements the IO driver_module. " "Built-in drivers located in carlos.edge.device.driver module " "don't need to specify the full path. Each driver_module module" From cf189aa9f38340882f3139f1553534170a3f74aa Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 20:18:37 +0200 Subject: [PATCH 02/10] create controller logic for metadata --- .../carlos/database/device/__init__.py | 26 +- .../carlos/database/device/device_metadata.py | 333 ++++++++++++++++++ lib/py_carlos_database/carlos/database/orm.py | 2 + 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 lib/py_carlos_database/carlos/database/device/device_metadata.py diff --git a/lib/py_carlos_database/carlos/database/device/__init__.py b/lib/py_carlos_database/carlos/database/device/__init__.py index 6428184a..c88943f1 100644 --- a/lib/py_carlos_database/carlos/database/device/__init__.py +++ b/lib/py_carlos_database/carlos/database/device/__init__.py @@ -1,17 +1,28 @@ __all__ = [ "CarlosDevice", "CarlosDeviceCreate", + "CarlosDeviceDriver", + "CarlosDeviceSignal", + "CarlosDeviceSignalMutation", "CarlosDeviceUpdate", "create_device", + "create_device_driver", + "create_device_signals", + "delete_device_driver", + "delete_device_signal", "does_device_exist", "ensure_device_exists", "get_device", + "get_device_drivers", + "get_device_signals", "list_devices", "set_device_seen", "update_device", + "update_device_driver", + "update_device_signal", ] -from carlos.database.device.device_management import ( +from .device_management import ( CarlosDevice, CarlosDeviceCreate, CarlosDeviceUpdate, @@ -23,3 +34,16 @@ set_device_seen, update_device, ) +from .device_metadata import ( + CarlosDeviceDriver, + CarlosDeviceSignal, + CarlosDeviceSignalMutation, + create_device_driver, + create_device_signals, + delete_device_driver, + delete_device_signal, + get_device_drivers, + get_device_signals, + update_device_driver, + update_device_signal, +) diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata.py b/lib/py_carlos_database/carlos/database/device/device_metadata.py new file mode 100644 index 00000000..c314fd1a --- /dev/null +++ b/lib/py_carlos_database/carlos/database/device/device_metadata.py @@ -0,0 +1,333 @@ +__all__ = [ + "CarlosDeviceDriver", + "CarlosDeviceSignal", + "CarlosDeviceSignalMutation", + "create_device_driver", + "create_device_signals", + "delete_device_driver", + "delete_device_signal", + "get_device_drivers", + "get_device_signals", + "update_device_driver", + "update_device_signal", +] + +from uuid import UUID + +from carlos.edge.interface import DeviceId +from carlos.edge.interface.device.driver_config import ( + DRIVER_IDENTIFIER_LENGTH, + DriverDirection, +) +from carlos.edge.interface.units import UnitOfMeasurement +from pydantic import Field +from sqlalchemy import delete, insert, select, update +from sqlalchemy.exc import NoResultFound + +from carlos.database.context import RequestContext +from carlos.database.exceptions import NotFound +from carlos.database.orm import ( + CarlosDeviceDriverOrm, + CarlosDeviceOrm, + CarlosDeviceSignalOrm, +) +from carlos.database.schema import CarlosSchema +from carlos.database.utils import does_exist + + +class CarlosDeviceDriver(CarlosSchema): + + device_id: UUID = Field(..., description="The device the driver belongs to.") + driver_identifier: str = Field( + ..., + min_length=1, + max_length=DRIVER_IDENTIFIER_LENGTH, + description="The unique identifier of the driver in the context of the device.", + ) + direction: DriverDirection = Field(..., description="The direction of the IO.") + + driver_module: str = Field( + ..., + min_length=1, + max_length=255, + description="The module that implements the IO driver.", + ) + + display_name: str = Field( + ..., + min_length=1, + max_length=255, + description="The name of the driver that is displayed in the UI.", + ) + + is_visible_on_dashboard: bool = Field( + ..., + description="Whether the driver is visible on the dashboard.", + ) + + +class _SignalMixin(CarlosSchema): + + device_id: UUID = Field(..., description="The device the driver belongs to.") + driver_identifier: str = Field( + ..., + min_length=1, + max_length=DRIVER_IDENTIFIER_LENGTH, + description="The unique identifier of the driver in the context of the device.", + ) + signal_identifier: str = Field( + ..., + min_length=1, + max_length=DRIVER_IDENTIFIER_LENGTH, + description="The unique identifier of the signal in the context of the driver.", + ) + + display_name: str = Field( + ..., + min_length=1, + max_length=255, + description="The name of the signal that is displayed in the UI.", + ) + + unit_of_measurement: UnitOfMeasurement = Field( + ..., + description="The unit of measurement of the signal.", + ) + + is_visible_on_dashboard: bool = Field( + ..., + description="Whether the signal is visible on the dashboard.", + ) + + +class CarlosDeviceSignalMutation(_SignalMixin): + """The properties required to create or update a device signal.""" + + pass + + +class CarlosDeviceSignal(_SignalMixin): + """The properties of a device signal.""" + + timeseries_id: int = Field(..., description="The unique identifier of the signal.") + + +async def get_device_drivers( + context: RequestContext, + device_id: DeviceId, +) -> list[CarlosDeviceDriver]: + """Returns all drivers registered for a given device. + + :param context: The request context. + :param device_id: The unique identifier of the device. + :return: A list of drivers registered for the device. + :raises NotFound: If the device does not exist. + """ + + query = select(CarlosDeviceDriverOrm).where( + CarlosDeviceDriverOrm.device_id == device_id + ) + + drivers = (await context.connection.execute(query)).all() + + if not drivers and await does_exist( + context, [CarlosDeviceOrm.device_id == device_id] + ): + raise NotFound(f"Device {device_id} does not exist.") + + return [CarlosDeviceDriver.model_validate(driver) for driver in drivers] + + +async def create_device_driver( + context: RequestContext, + driver: CarlosDeviceDriver, +) -> CarlosDeviceDriver: + """Creates a new driver for a given device. + + :param context: The request context. + :param driver: The properties of the driver to create. + :return: The properties of the created driver. + :raises NotFound: If the device does not exist. + """ + + stmt = ( + insert(CarlosDeviceDriverOrm) + .values( + **driver.model_dump(), + ) + .returning(CarlosDeviceDriverOrm) + ) + + driver = (await context.connection.execute(stmt)).one() + await context.connection.commit() + + return CarlosDeviceDriver.model_validate(driver) + + +async def update_device_driver( + context: RequestContext, + device_id: DeviceId, + driver_identifier: str, + driver: CarlosDeviceDriver, +) -> CarlosDeviceDriver: + """Updates a driver for a given device. + + :param context: The request context. + :param device_id: The unique identifier of the device. + :param driver_identifier: The unique identifier of the driver. + :param driver: The properties of the driver to update. + :return: The properties of the updated driver. + :raises NotFound: If the device or driver does not exist. + """ + + stmt = ( + update(CarlosDeviceDriverOrm) + .where( + CarlosDeviceDriverOrm.device_id == device_id, + CarlosDeviceDriverOrm.driver_identifier == driver_identifier, + ) + .values( + **driver.model_dump(), + ) + .returning(CarlosDeviceDriverOrm) + ) + + try: + driver = (await context.connection.execute(stmt)).one() + await context.connection.commit() + except NoResultFound: + raise NotFound( + f"Driver {driver_identifier=} does not exist for device {device_id=}." + ) + + return CarlosDeviceDriver.model_validate(driver) + + +async def delete_device_driver( + context: RequestContext, + device_id: DeviceId, + driver_identifier: str, +): + """Deletes a driver for a given device. + + :param context: The request context. + :param device_id: The unique identifier of the device. + :param driver_identifier: The unique identifier of the driver. + """ + + stmt = delete(CarlosDeviceDriverOrm).where( + CarlosDeviceDriverOrm.device_id == device_id, + CarlosDeviceDriverOrm.driver_identifier == driver_identifier, + ) + + await context.connection.execute(stmt) + await context.connection.commit() + + +async def get_device_signals( + context: RequestContext, + device_id: DeviceId, + driver_identifier: str, +) -> list[CarlosDeviceSignal]: + """Returns all signals registered for a given driver. + + :param context: The request context. + :param device_id: The unique identifier of the device. + :param driver_identifier: The unique identifier of the driver. + :return: A list of signals registered for the driver. + :raises NotFound: If the device or driver does not exist. + """ + + query = select(CarlosDeviceSignalOrm).where( + CarlosDeviceSignalOrm.device_id == device_id, + CarlosDeviceSignalOrm.driver_identifier == driver_identifier, + ) + + signals = (await context.connection.execute(query)).all() + + if not signals and await does_exist( + context, + [ + CarlosDeviceDriverOrm.device_id == device_id, + CarlosDeviceDriverOrm.driver_identifier == driver_identifier, + ], + ): + raise NotFound( + f"Driver {driver_identifier=} does not exist for device {device_id=}." + ) + + return [CarlosDeviceSignal.model_validate(signal) for signal in signals] + + +async def create_device_signals( + context: RequestContext, + signals: list[CarlosDeviceSignalMutation], +) -> list[CarlosDeviceSignal]: + """Creates new signals for a given driver. + + :param context: The request context. + :param signals: The properties of the signals to create. + :return: The properties of the created signals. + :raises NotFound: If the device or driver does not exist. + """ + + stmt = ( + insert(CarlosDeviceSignalOrm) + .values([signal.model_dump() for signal in signals]) + .returning(CarlosDeviceSignalOrm) + ) + + signals = (await context.connection.execute(stmt)).all() + await context.connection.commit() + + return [CarlosDeviceSignal.model_validate(signal) for signal in signals] + + +async def update_device_signal( + context: RequestContext, + time_series_id: int, + signal: CarlosDeviceSignalMutation, +) -> CarlosDeviceSignal: + """Updates a signal for a given driver. + + :param context: The request context. + :param time_series_id: The unique identifier of the signal. + :param signal: The properties of the signal to update. + :return: The properties of the updated signal. + :raises NotFound: If the device, driver, or signal does not exist. + """ + + stmt = ( + update(CarlosDeviceSignalOrm) + .where(CarlosDeviceSignalOrm.timeseries_id == time_series_id) + .values( + **signal.model_dump(), + ) + .returning(CarlosDeviceSignalOrm) + ) + + try: + signal = (await context.connection.execute(stmt)).one() + await context.connection.commit() + except NoResultFound: + raise NotFound(f"Signal {time_series_id=} does not exist.") + + return CarlosDeviceSignal.model_validate(signal) + + +async def delete_device_signal( + context: RequestContext, + time_series_id: int, +): + """Deletes a signal for a given driver. + + :param context: The request context. + :param time_series_id: The unique identifier of the signal. + """ + + stmt = delete(CarlosDeviceSignalOrm).where( + CarlosDeviceSignalOrm.timeseries_id == time_series_id + ) + + await context.connection.execute(stmt) + await context.connection.commit() diff --git a/lib/py_carlos_database/carlos/database/orm.py b/lib/py_carlos_database/carlos/database/orm.py index 079b9703..df7e05f0 100644 --- a/lib/py_carlos_database/carlos/database/orm.py +++ b/lib/py_carlos_database/carlos/database/orm.py @@ -1,6 +1,8 @@ __all__ = [ "ALL_SCHEMA_NAMES", + "CarlosDeviceDriverOrm", "CarlosDeviceOrm", + "CarlosDeviceSignalOrm", "CarlosModelBase", ] From a7e9a86821ca8b79b4405a1bd5bd481270fdeddf Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 21:51:49 +0200 Subject: [PATCH 03/10] refactor a bit --- .../carlos/database/device/__init__.py | 12 ++- .../carlos/database/device/device_metadata.py | 98 ++++++++++++------- services/api/carlos/api/routes/__init__.py | 3 +- .../api/carlos/api/routes/devices_routes.py | 66 ++++++++++++- .../api/carlos/api/routes/signals_routes.py | 32 ++++++ services/api/poetry.lock | 2 +- 6 files changed, 173 insertions(+), 40 deletions(-) create mode 100644 services/api/carlos/api/routes/signals_routes.py diff --git a/lib/py_carlos_database/carlos/database/device/__init__.py b/lib/py_carlos_database/carlos/database/device/__init__.py index c88943f1..5ff0e59a 100644 --- a/lib/py_carlos_database/carlos/database/device/__init__.py +++ b/lib/py_carlos_database/carlos/database/device/__init__.py @@ -3,7 +3,12 @@ "CarlosDeviceCreate", "CarlosDeviceDriver", "CarlosDeviceSignal", - "CarlosDeviceSignalMutation", + "CarlosDeviceDriver", + "CarlosDeviceDriverCreate", + "CarlosDeviceDriverUpdate", + "CarlosDeviceSignal", + "CarlosDeviceSignalCreate", + "CarlosDeviceSignalUpdate", "CarlosDeviceUpdate", "create_device", "create_device_driver", @@ -36,8 +41,11 @@ ) from .device_metadata import ( CarlosDeviceDriver, + CarlosDeviceDriverCreate, + CarlosDeviceDriverUpdate, CarlosDeviceSignal, - CarlosDeviceSignalMutation, + CarlosDeviceSignalCreate, + CarlosDeviceSignalUpdate, create_device_driver, create_device_signals, delete_device_driver, diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata.py b/lib/py_carlos_database/carlos/database/device/device_metadata.py index c314fd1a..3d09d632 100644 --- a/lib/py_carlos_database/carlos/database/device/device_metadata.py +++ b/lib/py_carlos_database/carlos/database/device/device_metadata.py @@ -1,7 +1,10 @@ __all__ = [ "CarlosDeviceDriver", + "CarlosDeviceDriverCreate", + "CarlosDeviceDriverUpdate", "CarlosDeviceSignal", - "CarlosDeviceSignalMutation", + "CarlosDeviceSignalCreate", + "CarlosDeviceSignalUpdate", "create_device_driver", "create_device_signals", "delete_device_driver", @@ -35,24 +38,7 @@ from carlos.database.utils import does_exist -class CarlosDeviceDriver(CarlosSchema): - - device_id: UUID = Field(..., description="The device the driver belongs to.") - driver_identifier: str = Field( - ..., - min_length=1, - max_length=DRIVER_IDENTIFIER_LENGTH, - description="The unique identifier of the driver in the context of the device.", - ) - direction: DriverDirection = Field(..., description="The direction of the IO.") - - driver_module: str = Field( - ..., - min_length=1, - max_length=255, - description="The module that implements the IO driver.", - ) - +class _DriverMixin(CarlosSchema): display_name: str = Field( ..., min_length=1, @@ -66,22 +52,38 @@ class CarlosDeviceDriver(CarlosSchema): ) -class _SignalMixin(CarlosSchema): +class CarlosDeviceDriverCreate(_DriverMixin): + """The properties required to create a device driver.""" - device_id: UUID = Field(..., description="The device the driver belongs to.") driver_identifier: str = Field( ..., min_length=1, max_length=DRIVER_IDENTIFIER_LENGTH, description="The unique identifier of the driver in the context of the device.", ) - signal_identifier: str = Field( + direction: DriverDirection = Field(..., description="The direction of the IO.") + + driver_module: str = Field( ..., min_length=1, - max_length=DRIVER_IDENTIFIER_LENGTH, - description="The unique identifier of the signal in the context of the driver.", + max_length=255, + description="The module that implements the IO driver.", ) + +class CarlosDeviceDriverUpdate(_DriverMixin): + """The properties required to update a device.""" + + pass + + +class CarlosDeviceDriver(CarlosDeviceDriverCreate): + + device_id: UUID = Field(..., description="The device the driver belongs to.") + + +class _SignalMixin(CarlosSchema): + display_name: str = Field( ..., min_length=1, @@ -100,16 +102,32 @@ class _SignalMixin(CarlosSchema): ) -class CarlosDeviceSignalMutation(_SignalMixin): +class CarlosDeviceSignalCreate(_SignalMixin): """The properties required to create or update a device signal.""" - pass + signal_identifier: str = Field( + ..., + min_length=1, + max_length=DRIVER_IDENTIFIER_LENGTH, + description="The unique identifier of the signal in the context of the driver.", + ) + + +class CarlosDeviceSignalUpdate(_SignalMixin): + """The properties required to update a device signal.""" class CarlosDeviceSignal(_SignalMixin): """The properties of a device signal.""" timeseries_id: int = Field(..., description="The unique identifier of the signal.") + device_id: UUID = Field(..., description="The device the driver belongs to.") + driver_identifier: str = Field( + ..., + min_length=1, + max_length=DRIVER_IDENTIFIER_LENGTH, + description="The unique identifier of the driver in the context of the device.", + ) async def get_device_drivers( @@ -140,7 +158,7 @@ async def get_device_drivers( async def create_device_driver( context: RequestContext, - driver: CarlosDeviceDriver, + driver: CarlosDeviceDriverCreate, ) -> CarlosDeviceDriver: """Creates a new driver for a given device. @@ -168,7 +186,7 @@ async def update_device_driver( context: RequestContext, device_id: DeviceId, driver_identifier: str, - driver: CarlosDeviceDriver, + driver: CarlosDeviceDriverUpdate, ) -> CarlosDeviceDriver: """Updates a driver for a given device. @@ -261,11 +279,15 @@ async def get_device_signals( async def create_device_signals( context: RequestContext, - signals: list[CarlosDeviceSignalMutation], + device_id: DeviceId, + driver_identifier: str, + signals: list[CarlosDeviceSignalCreate], ) -> list[CarlosDeviceSignal]: """Creates new signals for a given driver. :param context: The request context. + :param device_id: The unique identifier of the device. + :param driver_identifier: The unique identifier of the driver. :param signals: The properties of the signals to create. :return: The properties of the created signals. :raises NotFound: If the device or driver does not exist. @@ -273,7 +295,13 @@ async def create_device_signals( stmt = ( insert(CarlosDeviceSignalOrm) - .values([signal.model_dump() for signal in signals]) + .values( + [ + signal.model_dump() + | {"device_id": device_id, "driver_identifier": driver_identifier} + for signal in signals + ] + ) .returning(CarlosDeviceSignalOrm) ) @@ -285,13 +313,13 @@ async def create_device_signals( async def update_device_signal( context: RequestContext, - time_series_id: int, - signal: CarlosDeviceSignalMutation, + timeseries_id: int, + signal: CarlosDeviceSignalUpdate, ) -> CarlosDeviceSignal: """Updates a signal for a given driver. :param context: The request context. - :param time_series_id: The unique identifier of the signal. + :param timeseries_id: The unique identifier of the signal. :param signal: The properties of the signal to update. :return: The properties of the updated signal. :raises NotFound: If the device, driver, or signal does not exist. @@ -299,7 +327,7 @@ async def update_device_signal( stmt = ( update(CarlosDeviceSignalOrm) - .where(CarlosDeviceSignalOrm.timeseries_id == time_series_id) + .where(CarlosDeviceSignalOrm.timeseries_id == timeseries_id) .values( **signal.model_dump(), ) @@ -310,7 +338,7 @@ async def update_device_signal( signal = (await context.connection.execute(stmt)).one() await context.connection.commit() except NoResultFound: - raise NotFound(f"Signal {time_series_id=} does not exist.") + raise NotFound(f"Signal {timeseries_id=} does not exist.") return CarlosDeviceSignal.model_validate(signal) diff --git a/services/api/carlos/api/routes/__init__.py b/services/api/carlos/api/routes/__init__.py index 11f82968..b128d51a 100644 --- a/services/api/carlos/api/routes/__init__.py +++ b/services/api/carlos/api/routes/__init__.py @@ -7,11 +7,12 @@ from .device_server_routes import device_server_router from .devices_routes import devices_router from .health_routes import health_router +from .signals_routes import signals_router main_router = APIRouter(dependencies=[Security(verify_token)]) """This is the main router for the API. It is for routes that require authentication.""" main_router.include_router(devices_router, prefix="/devices", tags=["devices"]) - +main_router.include_router(signals_router, prefix="/signals", tags=["signals"]) public_router = APIRouter() """This route is for routes that are public and do not require authentication.""" diff --git a/services/api/carlos/api/routes/devices_routes.py b/services/api/carlos/api/routes/devices_routes.py index 4b7cbd6a..fdd5d72b 100644 --- a/services/api/carlos/api/routes/devices_routes.py +++ b/services/api/carlos/api/routes/devices_routes.py @@ -4,14 +4,22 @@ from carlos.database.device import ( CarlosDevice, CarlosDeviceCreate, + CarlosDeviceDriver, CarlosDeviceUpdate, create_device, get_device, + get_device_drivers, list_devices, update_device, + update_device_driver, +) +from carlos.database.device.device_metadata import ( + CarlosDeviceDriverUpdate, + CarlosDeviceSignal, + get_device_signals, ) from carlos.edge.interface import DeviceId -from fastapi import APIRouter, Depends, Path +from fastapi import APIRouter, Body, Depends, Path from carlos.api.depends.context import request_context @@ -63,3 +71,59 @@ async def update_device_route( ): """Update a device by its ID.""" return await update_device(context=context, device_id=device_id, device=device) + + +@devices_router.get( + "/{deviceId}/drivers", + summary="Get all drivers for a device.", + response_model=list[CarlosDeviceDriver], +) +async def get_device_drivers_route( + device_id: DeviceId = DEVICE_ID_PATH, + context: RequestContext = Depends(request_context), +): + """Get all drivers for a device.""" + return await get_device_drivers(context=context, device_id=device_id) + + +DRIVER_IDENTIFIER_PATH: str = Path( + ..., + alias="driverIdentifier", + description="The unique identifier of the driver.", +) + + +@devices_router.put( + "/{deviceId}/drivers/{driverIdentifier}", + summary="Update a driver for a device.", + response_model=CarlosDeviceDriver, +) +async def update_device_driver_route( + driver: CarlosDeviceDriverUpdate = Body(), + device_id: DeviceId = DEVICE_ID_PATH, + driver_identifier: str = DRIVER_IDENTIFIER_PATH, + context: RequestContext = Depends(request_context), +): + """Update a driver for a device.""" + return await update_device_driver( + context=context, + device_id=device_id, + driver_identifier=driver_identifier, + driver=driver, + ) + + +@devices_router.get( + "/{deviceId}/drivers/{driverIdentifier}/signals", + summary="Get all signals for a driver.", + response_model=list[CarlosDeviceSignal], +) +async def get_device_signals_route( + device_id: DeviceId = DEVICE_ID_PATH, + driver_identifier: str = DRIVER_IDENTIFIER_PATH, + context: RequestContext = Depends(request_context), +): + """Get all signals for a driver.""" + return await get_device_signals( + context=context, device_id=device_id, driver_identifier=driver_identifier + ) diff --git a/services/api/carlos/api/routes/signals_routes.py b/services/api/carlos/api/routes/signals_routes.py new file mode 100644 index 00000000..238bb5f6 --- /dev/null +++ b/services/api/carlos/api/routes/signals_routes.py @@ -0,0 +1,32 @@ +__all__ = ["signals_router"] +from carlos.database.context import RequestContext +from carlos.database.device import ( + CarlosDeviceSignal, + CarlosDeviceSignalUpdate, + update_device_signal, +) +from fastapi import APIRouter, Body, Depends, Path + +from carlos.api.depends.context import request_context + +signals_router = APIRouter() + + +@signals_router.put( + "/{timeseriesId}", + summary="Update a signal by its ID.", + response_model=CarlosDeviceSignal, +) +async def update_device_signal_route( + signal: CarlosDeviceSignalUpdate = Body(), + timeseries_id: int = Path( + ..., + alias="timeseriesId", + description="The unique identifier of the signal.", + ), + context: RequestContext = Depends(request_context), +): + """Update a signal by its ID.""" + return await update_device_signal( + context=context, timeseries_id=timeseries_id, signal=signal + ) diff --git a/services/api/poetry.lock b/services/api/poetry.lock index 94e17d72..dd5754f8 100644 --- a/services/api/poetry.lock +++ b/services/api/poetry.lock @@ -318,7 +318,7 @@ url = "../../lib/py_edge_interface" [[package]] name = "carlos-edge-server" -version = "0.1.1" +version = "0.1.2" description = "The library for the edge server of the carlos project." optional = false python-versions = ">=3.11,<3.12" From 769f915c9ae2fd404f0ecc197cae760d060e6082 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Apr 2024 19:52:46 +0000 Subject: [PATCH 04/10] =?UTF-8?q?Bump=20lib/py=5Fedge=5Finterface=20versio?= =?UTF-8?q?n:=200.1.5=20=E2=86=92=200.1.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/py_edge_interface/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_interface/pyproject.toml b/lib/py_edge_interface/pyproject.toml index 38ad328a..7066f6ed 100644 --- a/lib/py_edge_interface/pyproject.toml +++ b/lib/py_edge_interface/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "carlos.edge.interface" -version = "0.1.5" +version = "0.1.6" description = "Shared library to handle the edge communication." authors = ["Felix Fanghanel"] license = "MIT" @@ -23,7 +23,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.bumpversion] -current_version = "0.1.5" +current_version = "0.1.6" commit = true tag = false parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z0-9\\.]+))?" From e0d5a82bd003695e026d0dabc6defff38dbc62d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Apr 2024 19:52:50 +0000 Subject: [PATCH 05/10] =?UTF-8?q?Bump=20lib/py=5Fcarlos=5Fdatabase=20versi?= =?UTF-8?q?on:=200.1.0=20=E2=86=92=200.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/py_carlos_database/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/py_carlos_database/pyproject.toml b/lib/py_carlos_database/pyproject.toml index cff2e9a8..31f12611 100644 --- a/lib/py_carlos_database/pyproject.toml +++ b/lib/py_carlos_database/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "carlos.database" -version = "0.1.0" +version = "0.1.1" description = "The library for the edge device of the carlos project." authors = ["Felix Fanghanel"] license = "MIT" @@ -30,7 +30,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.bumpversion] -current_version = "0.1.0" +current_version = "0.1.1" commit = true tag = false parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z0-9\\.]+))?" From 5828b2f1cdabbdd94e30ed0d04b90d9af60ac75a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Apr 2024 19:54:05 +0000 Subject: [PATCH 06/10] update services/api --- services/api/generated/openapi.json | 438 +++++++++++++++++++++++++++ services/frontend/src/api/openapi.ts | 267 ++++++++++++++++ 2 files changed, 705 insertions(+) diff --git a/services/api/generated/openapi.json b/services/api/generated/openapi.json index 03efd605..befd931c 100644 --- a/services/api/generated/openapi.json +++ b/services/api/generated/openapi.json @@ -191,6 +191,259 @@ } } }, + "/devices/{deviceId}/drivers": { + "get": { + "tags": [ + "devices" + ], + "summary": "Get all drivers for a device.", + "description": "Get all drivers for a device.", + "operationId": "getDeviceDriversRoute", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "deviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the device.", + "title": "Deviceid" + }, + "description": "The unique identifier of the device." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CarlosDeviceDriver" + }, + "title": "Response Getdevicedriversroute" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/devices/{deviceId}/drivers/{driverIdentifier}": { + "put": { + "tags": [ + "devices" + ], + "summary": "Update a driver for a device.", + "description": "Update a driver for a device.", + "operationId": "updateDeviceDriverRoute", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "deviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the device.", + "title": "Deviceid" + }, + "description": "The unique identifier of the device." + }, + { + "name": "driverIdentifier", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique identifier of the driver.", + "title": "Driveridentifier" + }, + "description": "The unique identifier of the driver." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CarlosDeviceDriverUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CarlosDeviceDriver" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/devices/{deviceId}/drivers/{driverIdentifier}/signals": { + "get": { + "tags": [ + "devices" + ], + "summary": "Get all signals for a driver.", + "description": "Get all signals for a driver.", + "operationId": "getDeviceSignalsRoute", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "deviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the device.", + "title": "Deviceid" + }, + "description": "The unique identifier of the device." + }, + { + "name": "driverIdentifier", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique identifier of the driver.", + "title": "Driveridentifier" + }, + "description": "The unique identifier of the driver." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CarlosDeviceSignal" + }, + "title": "Response Getdevicesignalsroute" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/signals/{timeseriesId}": { + "put": { + "tags": [ + "signals" + ], + "summary": "Update a signal by its ID.", + "description": "Update a signal by its ID.", + "operationId": "updateDeviceSignalRoute", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "timeseriesId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "The unique identifier of the signal.", + "title": "Timeseriesid" + }, + "description": "The unique identifier of the signal." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CarlosDeviceSignalUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CarlosDeviceSignal" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/health": { "get": { "tags": [ @@ -361,6 +614,168 @@ "title": "CarlosDeviceCreate", "description": "Allow you to create a new device." }, + "CarlosDeviceDriver": { + "properties": { + "displayName": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Displayname", + "description": "The name of the driver that is displayed in the UI." + }, + "isVisibleOnDashboard": { + "type": "boolean", + "title": "Isvisibleondashboard", + "description": "Whether the driver is visible on the dashboard." + }, + "driverIdentifier": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "title": "Driveridentifier", + "description": "The unique identifier of the driver in the context of the device." + }, + "direction": { + "allOf": [ + { + "$ref": "#/components/schemas/DriverDirection" + } + ], + "description": "The direction of the IO." + }, + "driverModule": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Drivermodule", + "description": "The module that implements the IO driver." + }, + "deviceId": { + "type": "string", + "format": "uuid", + "title": "Deviceid", + "description": "The device the driver belongs to." + } + }, + "type": "object", + "required": [ + "displayName", + "isVisibleOnDashboard", + "driverIdentifier", + "direction", + "driverModule", + "deviceId" + ], + "title": "CarlosDeviceDriver" + }, + "CarlosDeviceDriverUpdate": { + "properties": { + "displayName": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Displayname", + "description": "The name of the driver that is displayed in the UI." + }, + "isVisibleOnDashboard": { + "type": "boolean", + "title": "Isvisibleondashboard", + "description": "Whether the driver is visible on the dashboard." + } + }, + "type": "object", + "required": [ + "displayName", + "isVisibleOnDashboard" + ], + "title": "CarlosDeviceDriverUpdate", + "description": "The properties required to update a device." + }, + "CarlosDeviceSignal": { + "properties": { + "displayName": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Displayname", + "description": "The name of the signal that is displayed in the UI." + }, + "unitOfMeasurement": { + "allOf": [ + { + "$ref": "#/components/schemas/UnitOfMeasurement" + } + ], + "description": "The unit of measurement of the signal." + }, + "isVisibleOnDashboard": { + "type": "boolean", + "title": "Isvisibleondashboard", + "description": "Whether the signal is visible on the dashboard." + }, + "timeseriesId": { + "type": "integer", + "title": "Timeseriesid", + "description": "The unique identifier of the signal." + }, + "deviceId": { + "type": "string", + "format": "uuid", + "title": "Deviceid", + "description": "The device the driver belongs to." + }, + "driverIdentifier": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "title": "Driveridentifier", + "description": "The unique identifier of the driver in the context of the device." + } + }, + "type": "object", + "required": [ + "displayName", + "unitOfMeasurement", + "isVisibleOnDashboard", + "timeseriesId", + "deviceId", + "driverIdentifier" + ], + "title": "CarlosDeviceSignal", + "description": "The properties of a device signal." + }, + "CarlosDeviceSignalUpdate": { + "properties": { + "displayName": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Displayname", + "description": "The name of the signal that is displayed in the UI." + }, + "unitOfMeasurement": { + "allOf": [ + { + "$ref": "#/components/schemas/UnitOfMeasurement" + } + ], + "description": "The unit of measurement of the signal." + }, + "isVisibleOnDashboard": { + "type": "boolean", + "title": "Isvisibleondashboard", + "description": "Whether the signal is visible on the dashboard." + } + }, + "type": "object", + "required": [ + "displayName", + "unitOfMeasurement", + "isVisibleOnDashboard" + ], + "title": "CarlosDeviceSignalUpdate", + "description": "The properties required to update a device signal." + }, "CarlosDeviceUpdate": { "properties": { "displayName": { @@ -390,6 +805,16 @@ "title": "CarlosDeviceUpdate", "description": "Allows you to update the device information." }, + "DriverDirection": { + "type": "string", + "enum": [ + "input", + "output", + "bidirectional" + ], + "title": "DriverDirection", + "description": "Enum for the direction of the IO." + }, "HTTPValidationError": { "properties": { "detail": { @@ -432,6 +857,19 @@ "title": "HealthStatus", "description": "The status of the API." }, + "UnitOfMeasurement": { + "type": "integer", + "enum": [ + 0, + 100, + 200, + 201, + 300, + 400 + ], + "title": "UnitOfMeasurement", + "description": "An enumeration of supported units of measurement.\n\nThe values of this enumeration are based on the PhysicalQuantity enumeration.\n\n\n- 0 = UNIT_LESS\n- 100 = PERCENTAGE\n- 200 = CELSIUS\n- 201 = FAHRENHEIT\n- 300 = HUMIDITY_PERCENTAGE\n- 400 = LUX" + }, "ValidationError": { "properties": { "loc": { diff --git a/services/frontend/src/api/openapi.ts b/services/frontend/src/api/openapi.ts index 8219bcfd..2edcfbe7 100644 --- a/services/frontend/src/api/openapi.ts +++ b/services/frontend/src/api/openapi.ts @@ -29,6 +29,34 @@ export interface paths { */ put: operations["updateDeviceRoute"]; }; + "/devices/{deviceId}/drivers": { + /** + * Get all drivers for a device. + * @description Get all drivers for a device. + */ + get: operations["getDeviceDriversRoute"]; + }; + "/devices/{deviceId}/drivers/{driverIdentifier}": { + /** + * Update a driver for a device. + * @description Update a driver for a device. + */ + put: operations["updateDeviceDriverRoute"]; + }; + "/devices/{deviceId}/drivers/{driverIdentifier}/signals": { + /** + * Get all signals for a driver. + * @description Get all signals for a driver. + */ + get: operations["getDeviceSignalsRoute"]; + }; + "/signals/{timeseriesId}": { + /** + * Update a signal by its ID. + * @description Update a signal by its ID. + */ + put: operations["updateDeviceSignalRoute"]; + }; "/health": { /** * Health @@ -103,6 +131,105 @@ export interface components { */ description?: string | null; }; + /** CarlosDeviceDriver */ + CarlosDeviceDriver: { + /** + * Displayname + * @description The name of the driver that is displayed in the UI. + */ + displayName: string; + /** + * Isvisibleondashboard + * @description Whether the driver is visible on the dashboard. + */ + isVisibleOnDashboard: boolean; + /** + * Driveridentifier + * @description The unique identifier of the driver in the context of the device. + */ + driverIdentifier: string; + /** @description The direction of the IO. */ + direction: components["schemas"]["DriverDirection"]; + /** + * Drivermodule + * @description The module that implements the IO driver. + */ + driverModule: string; + /** + * Deviceid + * Format: uuid + * @description The device the driver belongs to. + */ + deviceId: string; + }; + /** + * CarlosDeviceDriverUpdate + * @description The properties required to update a device. + */ + CarlosDeviceDriverUpdate: { + /** + * Displayname + * @description The name of the driver that is displayed in the UI. + */ + displayName: string; + /** + * Isvisibleondashboard + * @description Whether the driver is visible on the dashboard. + */ + isVisibleOnDashboard: boolean; + }; + /** + * CarlosDeviceSignal + * @description The properties of a device signal. + */ + CarlosDeviceSignal: { + /** + * Displayname + * @description The name of the signal that is displayed in the UI. + */ + displayName: string; + /** @description The unit of measurement of the signal. */ + unitOfMeasurement: components["schemas"]["UnitOfMeasurement"]; + /** + * Isvisibleondashboard + * @description Whether the signal is visible on the dashboard. + */ + isVisibleOnDashboard: boolean; + /** + * Timeseriesid + * @description The unique identifier of the signal. + */ + timeseriesId: number; + /** + * Deviceid + * Format: uuid + * @description The device the driver belongs to. + */ + deviceId: string; + /** + * Driveridentifier + * @description The unique identifier of the driver in the context of the device. + */ + driverIdentifier: string; + }; + /** + * CarlosDeviceSignalUpdate + * @description The properties required to update a device signal. + */ + CarlosDeviceSignalUpdate: { + /** + * Displayname + * @description The name of the signal that is displayed in the UI. + */ + displayName: string; + /** @description The unit of measurement of the signal. */ + unitOfMeasurement: components["schemas"]["UnitOfMeasurement"]; + /** + * Isvisibleondashboard + * @description Whether the signal is visible on the dashboard. + */ + isVisibleOnDashboard: boolean; + }; /** * CarlosDeviceUpdate * @description Allows you to update the device information. @@ -119,6 +246,12 @@ export interface components { */ description?: string | null; }; + /** + * DriverDirection + * @description Enum for the direction of the IO. + * @enum {string} + */ + DriverDirection: "input" | "output" | "bidirectional"; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -142,6 +275,22 @@ export interface components { * @enum {string} */ HealthStatus: "ok" | "no_db_connection" | "error"; + /** + * UnitOfMeasurement + * @description An enumeration of supported units of measurement. + * + * The values of this enumeration are based on the PhysicalQuantity enumeration. + * + * + * - 0 = UNIT_LESS + * - 100 = PERCENTAGE + * - 200 = CELSIUS + * - 201 = FAHRENHEIT + * - 300 = HUMIDITY_PERCENTAGE + * - 400 = LUX + * @enum {integer} + */ + UnitOfMeasurement: 0 | 100 | 200 | 201 | 300 | 400; /** ValidationError */ ValidationError: { /** Location */ @@ -261,6 +410,124 @@ export interface operations { }; }; }; + /** + * Get all drivers for a device. + * @description Get all drivers for a device. + */ + getDeviceDriversRoute: { + parameters: { + path: { + /** @description The unique identifier of the device. */ + deviceId: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["CarlosDeviceDriver"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Update a driver for a device. + * @description Update a driver for a device. + */ + updateDeviceDriverRoute: { + parameters: { + path: { + /** @description The unique identifier of the device. */ + deviceId: string; + /** @description The unique identifier of the driver. */ + driverIdentifier: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CarlosDeviceDriverUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["CarlosDeviceDriver"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Get all signals for a driver. + * @description Get all signals for a driver. + */ + getDeviceSignalsRoute: { + parameters: { + path: { + /** @description The unique identifier of the device. */ + deviceId: string; + /** @description The unique identifier of the driver. */ + driverIdentifier: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["CarlosDeviceSignal"][]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Update a signal by its ID. + * @description Update a signal by its ID. + */ + updateDeviceSignalRoute: { + parameters: { + path: { + /** @description The unique identifier of the signal. */ + timeseriesId: number; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CarlosDeviceSignalUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["CarlosDeviceSignal"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * Health * @description Endpoint to determine the health of the API. From eb6bb3ed633875df26f06737fafa3dcc81fb6b99 Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 21:56:04 +0200 Subject: [PATCH 07/10] update lock files --- services/api/poetry.lock | 4 ++-- services/device/poetry.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/api/poetry.lock b/services/api/poetry.lock index dd5754f8..3dcf41eb 100644 --- a/services/api/poetry.lock +++ b/services/api/poetry.lock @@ -253,7 +253,7 @@ test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-m [[package]] name = "carlos-database" -version = "0.1.0" +version = "0.1.1" description = "The library for the edge device of the carlos project." optional = false python-versions = ">=3.11,<3.12" @@ -302,7 +302,7 @@ url = "../../lib/py_edge_device" [[package]] name = "carlos-edge-interface" -version = "0.1.5" +version = "0.1.6" description = "Shared library to handle the edge communication." optional = false python-versions = ">=3.11,<3.12" diff --git a/services/device/poetry.lock b/services/device/poetry.lock index 0fdfc9c3..ff3b4d32 100644 --- a/services/device/poetry.lock +++ b/services/device/poetry.lock @@ -212,7 +212,7 @@ url = "../../lib/py_edge_device" [[package]] name = "carlos-edge-interface" -version = "0.1.5" +version = "0.1.6" description = "Shared library to handle the edge communication." optional = false python-versions = ">=3.11,<3.12" From 9e57b2b468c138c36e8c95271947f10be3c105e4 Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 22:15:32 +0200 Subject: [PATCH 08/10] add tests for drivers --- .../carlos/database/device/device_metadata.py | 16 ++-- .../database/device/device_metadata_test.py | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 lib/py_carlos_database/carlos/database/device/device_metadata_test.py diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata.py b/lib/py_carlos_database/carlos/database/device/device_metadata.py index 3d09d632..d5e47ba3 100644 --- a/lib/py_carlos_database/carlos/database/device/device_metadata.py +++ b/lib/py_carlos_database/carlos/database/device/device_metadata.py @@ -176,10 +176,10 @@ async def create_device_driver( .returning(CarlosDeviceDriverOrm) ) - driver = (await context.connection.execute(stmt)).one() + created = (await context.connection.execute(stmt)).one() await context.connection.commit() - return CarlosDeviceDriver.model_validate(driver) + return CarlosDeviceDriver.model_validate(created) async def update_device_driver( @@ -211,14 +211,14 @@ async def update_device_driver( ) try: - driver = (await context.connection.execute(stmt)).one() + updated = (await context.connection.execute(stmt)).one() await context.connection.commit() except NoResultFound: raise NotFound( f"Driver {driver_identifier=} does not exist for device {device_id=}." ) - return CarlosDeviceDriver.model_validate(driver) + return CarlosDeviceDriver.model_validate(updated) async def delete_device_driver( @@ -305,10 +305,10 @@ async def create_device_signals( .returning(CarlosDeviceSignalOrm) ) - signals = (await context.connection.execute(stmt)).all() + created = (await context.connection.execute(stmt)).all() await context.connection.commit() - return [CarlosDeviceSignal.model_validate(signal) for signal in signals] + return [CarlosDeviceSignal.model_validate(signal) for signal in created] async def update_device_signal( @@ -335,12 +335,12 @@ async def update_device_signal( ) try: - signal = (await context.connection.execute(stmt)).one() + updated = (await context.connection.execute(stmt)).one() await context.connection.commit() except NoResultFound: raise NotFound(f"Signal {timeseries_id=} does not exist.") - return CarlosDeviceSignal.model_validate(signal) + return CarlosDeviceSignal.model_validate(updated) async def delete_device_signal( diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata_test.py b/lib/py_carlos_database/carlos/database/device/device_metadata_test.py new file mode 100644 index 00000000..3bd2932f --- /dev/null +++ b/lib/py_carlos_database/carlos/database/device/device_metadata_test.py @@ -0,0 +1,96 @@ +from carlos.edge.interface.device import DriverDirection + +from carlos.database.context import RequestContext +from carlos.database.device import ( + CarlosDeviceDriverCreate, + CarlosDeviceDriverUpdate, + create_device_driver, + delete_device_driver, + get_device_drivers, + update_device_driver, +) +from carlos.database.testing.expectations import DeviceId + + +async def test_driver_crud(async_carlos_db_context: RequestContext): + + device_id = DeviceId.DEVICE_A.value + + no_drivers = await get_device_drivers( + context=async_carlos_db_context, device_id=device_id + ) + assert len(no_drivers) == 0, "No drivers should be present" + + # CREATE #################################################################### + + to_create = CarlosDeviceDriverCreate( + display_name="My Driver", + is_visible_on_dashboard=True, + driver_identifier="my-driver", + direction=DriverDirection.INPUT, + driver_module="does_not_matter", + ) + + created = await create_device_driver( + context=async_carlos_db_context, driver=to_create + ) + + found = await get_device_drivers( + context=async_carlos_db_context, device_id=device_id + ) + assert len(found) == 1, "Should have 1 driver" + assert ( + CarlosDeviceDriverCreate.model_validate(found[0].model_dump()) == created + ), "Should be the same driver" + + # UPDATE #################################################################### + + to_update = CarlosDeviceDriverUpdate( + display_name="An updated displayName", is_visible_on_dashboard=False + ) + + updated = await update_device_driver( + context=async_carlos_db_context, + device_id=created.device_id, + driver_identifier=created.driver_identifier, + driver=to_update, + ) + + found = await get_device_drivers( + context=async_carlos_db_context, device_id=device_id + ) + + assert len(found) == 1, "Should have 1 driver" + + assert found[0] == updated, "Should be the same driver" + + assert found[0].display_name == "An updated displayName" + assert found[0].is_visible_on_dashboard is False + assert found[0].device_id == created.device_id, "device_id should not change" + assert ( + found[0].driver_identifier == created.driver_identifier + ), "driver_identifier should not change" + assert found[0].direction == created.direction, "direction should not change" + assert ( + found[0].driver_module == created.driver_module + ), "driver_module should not change" + + # DELETE #################################################################### + + await delete_device_driver( + context=async_carlos_db_context, + device_id=created.device_id, + driver_identifier=created.driver_identifier, + ) + + found = await get_device_drivers( + context=async_carlos_db_context, device_id=device_id + ) + assert len(found) == 0, "Should have 0 drivers" + + # deleting again should not raise an error + await delete_device_driver( + context=async_carlos_db_context, + device_id=created.device_id, + driver_identifier=created.driver_identifier, + ) From 1d9b4371273fb1763a76f376444f191510ed9b20 Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 22:46:48 +0200 Subject: [PATCH 09/10] add tests for signals --- .../carlos/database/device/device_metadata.py | 19 +- .../database/device/device_metadata_test.py | 185 +++++++++++++++++- lib/py_carlos_database/conftest.py | 4 - lib/py_carlos_database/pyproject.toml | 2 + 4 files changed, 194 insertions(+), 16 deletions(-) diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata.py b/lib/py_carlos_database/carlos/database/device/device_metadata.py index d5e47ba3..ef56f656 100644 --- a/lib/py_carlos_database/carlos/database/device/device_metadata.py +++ b/lib/py_carlos_database/carlos/database/device/device_metadata.py @@ -148,9 +148,8 @@ async def get_device_drivers( drivers = (await context.connection.execute(query)).all() - if not drivers and await does_exist( - context, [CarlosDeviceOrm.device_id == device_id] - ): + device_exists = await does_exist(context, [CarlosDeviceOrm.device_id == device_id]) + if not drivers and not device_exists: raise NotFound(f"Device {device_id} does not exist.") return [CarlosDeviceDriver.model_validate(driver) for driver in drivers] @@ -158,11 +157,13 @@ async def get_device_drivers( async def create_device_driver( context: RequestContext, + device_id: DeviceId, driver: CarlosDeviceDriverCreate, ) -> CarlosDeviceDriver: """Creates a new driver for a given device. :param context: The request context. + :param device_id: The unique identifier of the device. :param driver: The properties of the driver to create. :return: The properties of the created driver. :raises NotFound: If the device does not exist. @@ -171,6 +172,7 @@ async def create_device_driver( stmt = ( insert(CarlosDeviceDriverOrm) .values( + device_id=device_id, **driver.model_dump(), ) .returning(CarlosDeviceDriverOrm) @@ -263,13 +265,14 @@ async def get_device_signals( signals = (await context.connection.execute(query)).all() - if not signals and await does_exist( + driver_exists = await does_exist( context, [ CarlosDeviceDriverOrm.device_id == device_id, CarlosDeviceDriverOrm.driver_identifier == driver_identifier, ], - ): + ) + if not signals and not driver_exists: raise NotFound( f"Driver {driver_identifier=} does not exist for device {device_id=}." ) @@ -345,16 +348,16 @@ async def update_device_signal( async def delete_device_signal( context: RequestContext, - time_series_id: int, + timeseries_id: int, ): """Deletes a signal for a given driver. :param context: The request context. - :param time_series_id: The unique identifier of the signal. + :param timeseries_id: The unique identifier of the signal. """ stmt = delete(CarlosDeviceSignalOrm).where( - CarlosDeviceSignalOrm.timeseries_id == time_series_id + CarlosDeviceSignalOrm.timeseries_id == timeseries_id ) await context.connection.execute(stmt) diff --git a/lib/py_carlos_database/carlos/database/device/device_metadata_test.py b/lib/py_carlos_database/carlos/database/device/device_metadata_test.py index 3bd2932f..129f31ee 100644 --- a/lib/py_carlos_database/carlos/database/device/device_metadata_test.py +++ b/lib/py_carlos_database/carlos/database/device/device_metadata_test.py @@ -1,14 +1,25 @@ +import pytest +import pytest_asyncio from carlos.edge.interface.device import DriverDirection +from carlos.edge.interface.units import UnitOfMeasurement from carlos.database.context import RequestContext from carlos.database.device import ( + CarlosDeviceDriver, CarlosDeviceDriverCreate, CarlosDeviceDriverUpdate, + CarlosDeviceSignalCreate, + CarlosDeviceSignalUpdate, create_device_driver, + create_device_signals, delete_device_driver, + delete_device_signal, get_device_drivers, + get_device_signals, update_device_driver, + update_device_signal, ) +from carlos.database.exceptions import NotFound from carlos.database.testing.expectations import DeviceId @@ -21,6 +32,11 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): ) assert len(no_drivers) == 0, "No drivers should be present" + with pytest.raises(NotFound): + await get_device_drivers( + context=async_carlos_db_context, device_id=DeviceId.UNKNOWN.value + ) + # CREATE #################################################################### to_create = CarlosDeviceDriverCreate( @@ -32,7 +48,7 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): ) created = await create_device_driver( - context=async_carlos_db_context, driver=to_create + context=async_carlos_db_context, device_id=device_id, driver=to_create ) found = await get_device_drivers( @@ -40,8 +56,9 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): ) assert len(found) == 1, "Should have 1 driver" assert ( - CarlosDeviceDriverCreate.model_validate(found[0].model_dump()) == created - ), "Should be the same driver" + CarlosDeviceDriverCreate.model_validate(created.model_dump()) == to_create + ), "Should be the same driver as to_create" + assert found[0] == created, "Should be the same driver as created" # UPDATE #################################################################### @@ -51,7 +68,7 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): updated = await update_device_driver( context=async_carlos_db_context, - device_id=created.device_id, + device_id=device_id, driver_identifier=created.driver_identifier, driver=to_update, ) @@ -75,6 +92,21 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): found[0].driver_module == created.driver_module ), "driver_module should not change" + with pytest.raises(NotFound): + await update_device_driver( + context=async_carlos_db_context, + device_id=device_id, + driver_identifier="non-existent", + driver=to_update, + ) + with pytest.raises(NotFound): + await update_device_driver( + context=async_carlos_db_context, + device_id=DeviceId.UNKNOWN.value, + driver_identifier=created.driver_identifier, + driver=to_update, + ) + # DELETE #################################################################### await delete_device_driver( @@ -94,3 +126,148 @@ async def test_driver_crud(async_carlos_db_context: RequestContext): device_id=created.device_id, driver_identifier=created.driver_identifier, ) + + +@pytest_asyncio.fixture() +async def driver(async_carlos_db_context: RequestContext): + device_id = DeviceId.DEVICE_A.value + + to_create = CarlosDeviceDriverCreate( + display_name="My Driver", + is_visible_on_dashboard=True, + driver_identifier="my-driver", + direction=DriverDirection.INPUT, + driver_module="does_not_matter", + ) + + created = await create_device_driver( + context=async_carlos_db_context, device_id=device_id, driver=to_create + ) + + yield created + + await delete_device_driver( + context=async_carlos_db_context, + device_id=created.device_id, + driver_identifier=created.driver_identifier, + ) + + +async def test_driver_signals_crud( + async_carlos_db_context: RequestContext, driver: CarlosDeviceDriver +): + + no_signals = await get_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + ) + assert len(no_signals) == 0, "No signals should be present" + + with pytest.raises(NotFound): + await get_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier="non-existent", + ) + with pytest.raises(NotFound): + await get_device_signals( + context=async_carlos_db_context, + device_id=DeviceId.UNKNOWN.value, + driver_identifier=driver.driver_identifier, + ) + + # CREATE #################################################################### + + to_create = [ + CarlosDeviceSignalCreate( + display_name="Temperature", + unit_of_measurement=UnitOfMeasurement.CELSIUS, + is_visible_on_dashboard=True, + signal_identifier="temperature", + ), + CarlosDeviceSignalCreate( + display_name="Humidity", + unit_of_measurement=UnitOfMeasurement.HUMIDITY_PERCENTAGE, + is_visible_on_dashboard=True, + signal_identifier="humidity", + ), + ] + + created = await create_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + signals=to_create, + ) + + found = await get_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + ) + assert len(found) == 2, "Should have 2 signals" + assert sorted(found, key=lambda f: f.timeseries_id) == sorted( + created, key=lambda c: c.timeseries_id + ), "Should be the same signals" + + # UPDATE #################################################################### + + timeseries_id = created[0].timeseries_id + to_update = CarlosDeviceSignalUpdate( + display_name="Temperature Updated", + unit_of_measurement=UnitOfMeasurement.FAHRENHEIT, + is_visible_on_dashboard=False, + ) + + updated = await update_device_signal( + context=async_carlos_db_context, + timeseries_id=timeseries_id, + signal=to_update, + ) + + found = await get_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + ) + + assert len(found) == 2, "Should have 2 signals" + + assert updated.display_name == "Temperature Updated" + assert updated.unit_of_measurement == UnitOfMeasurement.FAHRENHEIT + assert updated.is_visible_on_dashboard is False + + found_map = {f.timeseries_id: f for f in found} + + assert found_map[timeseries_id] == updated, "Should be the same signal" + + with pytest.raises(NotFound): + await update_device_signal( + context=async_carlos_db_context, + timeseries_id=-1, + signal=to_update, + ) + + # DELETE #################################################################### + + await delete_device_signal( + context=async_carlos_db_context, + timeseries_id=created[0].timeseries_id, + ) + + found = await get_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + ) + assert len(found) == 1, "Should have 1 signal" + + # deleting again should not raise an error + await delete_device_signal( + context=async_carlos_db_context, + timeseries_id=created[0].timeseries_id, + ) + + # deliberately don't delete the last signal, as the delete cascade should take + # of the driver should care of it diff --git a/lib/py_carlos_database/conftest.py b/lib/py_carlos_database/conftest.py index a045e3c4..693451c7 100644 --- a/lib/py_carlos_database/conftest.py +++ b/lib/py_carlos_database/conftest.py @@ -1,8 +1,4 @@ -import asyncio -from typing import Iterator - import pytest -from _pytest.fixtures import FixtureRequest from carlos.database.migration import downgrade_carlos_schema, setup_test_db_data from carlos.database.plugin_pytest import connection_settings diff --git a/lib/py_carlos_database/pyproject.toml b/lib/py_carlos_database/pyproject.toml index 31f12611..d450000d 100644 --- a/lib/py_carlos_database/pyproject.toml +++ b/lib/py_carlos_database/pyproject.toml @@ -44,6 +44,8 @@ filename = "pyproject.toml" search = "version = \"{current_version}\"" replace = "version = \"{new_version}\"" +[tool.pytest.ini_options] +asyncio_mode = 'auto' [tool.isort] multi_line_output = 3 From b2b96909f21255d43671feb5a69bbea8e15e28f9 Mon Sep 17 00:00:00 2001 From: flxdot Date: Mon, 29 Apr 2024 23:23:54 +0200 Subject: [PATCH 10/10] api tests --- services/api/carlos/api/routes/conftest.py | 70 +++++++++++++++++++ .../carlos/api/routes/devices_routes_test.py | 45 ++++++++++++ .../carlos/api/routes/signals_routes_test.py | 25 +++++++ services/api/pyproject.toml | 3 + 4 files changed, 143 insertions(+) create mode 100644 services/api/carlos/api/routes/conftest.py create mode 100644 services/api/carlos/api/routes/signals_routes_test.py diff --git a/services/api/carlos/api/routes/conftest.py b/services/api/carlos/api/routes/conftest.py new file mode 100644 index 00000000..92c226ab --- /dev/null +++ b/services/api/carlos/api/routes/conftest.py @@ -0,0 +1,70 @@ +import pytest_asyncio +from carlos.database.context import RequestContext +from carlos.database.device import ( + CarlosDeviceDriver, + CarlosDeviceDriverCreate, + CarlosDeviceSignalCreate, + create_device_driver, + create_device_signals, + delete_device_driver, +) +from carlos.database.testing.expectations import DeviceId +from carlos.edge.interface.device import DriverDirection +from carlos.edge.interface.units import UnitOfMeasurement + + +@pytest_asyncio.fixture() +async def driver(async_carlos_db_context: RequestContext): + + device_id = DeviceId.DEVICE_A.value + + to_create = CarlosDeviceDriverCreate( + display_name="My Driver", + is_visible_on_dashboard=True, + driver_identifier="my-driver", + direction=DriverDirection.INPUT, + driver_module="does_not_matter", + ) + + created = await create_device_driver( + context=async_carlos_db_context, device_id=device_id, driver=to_create + ) + + yield created + + # cleanup: delete the driver + await delete_device_driver( + context=async_carlos_db_context, + device_id=device_id, + driver_identifier=created.driver_identifier, + ) + + +@pytest_asyncio.fixture() +async def driver_signals( + async_carlos_db_context: RequestContext, driver: CarlosDeviceDriver +): + + to_create = [ + CarlosDeviceSignalCreate( + display_name="Temperature", + unit_of_measurement=UnitOfMeasurement.CELSIUS, + is_visible_on_dashboard=True, + signal_identifier="temperature", + ), + CarlosDeviceSignalCreate( + display_name="Humidity", + unit_of_measurement=UnitOfMeasurement.HUMIDITY_PERCENTAGE, + is_visible_on_dashboard=True, + signal_identifier="humidity", + ), + ] + + created = await create_device_signals( + context=async_carlos_db_context, + device_id=driver.device_id, + driver_identifier=driver.driver_identifier, + signals=to_create, + ) + + yield created diff --git a/services/api/carlos/api/routes/devices_routes_test.py b/services/api/carlos/api/routes/devices_routes_test.py index a8d81b1f..90e852cb 100644 --- a/services/api/carlos/api/routes/devices_routes_test.py +++ b/services/api/carlos/api/routes/devices_routes_test.py @@ -1,5 +1,10 @@ from uuid import uuid4 +from carlos.database.device import ( + CarlosDeviceDriver, + CarlosDeviceDriverUpdate, + CarlosDeviceSignal, +) from carlos.database.device.device_management import ( CarlosDevice, CarlosDeviceCreate, @@ -51,3 +56,43 @@ def test_devices_crud(client: TestClient): updated_device = CarlosDevice.model_validate_json(response.content) assert updated_device.display_name == "updated_device" assert updated_device.description == "updated" + + +async def test_driver_routes(client: TestClient, driver: CarlosDeviceDriver): + + response = client.get(f"/devices/{driver.device_id}/drivers") + assert response.is_success, response.text + drivers = TypeAdapter(list[CarlosDeviceDriver]).validate_json(response.content) + assert len(drivers) == 1, "One driver should be present" + + # Update the device + + to_update = CarlosDeviceDriverUpdate( + display_name="My Driver (Updated)", + is_visible_on_dashboard=False, + ) + response = client.put( + f"/devices/{driver.device_id}/drivers/{driver.driver_identifier}", + content=to_update.model_dump_json(), + ) + assert response.is_success, response.text + updated = CarlosDeviceDriver.model_validate_json(response.content) + + assert updated.display_name == to_update.display_name + assert updated.is_visible_on_dashboard == to_update.is_visible_on_dashboard + + +async def test_get_device_signals_route( + client: TestClient, + driver: CarlosDeviceDriver, + driver_signals: list[CarlosDeviceSignal], +): + + response = client.get( + f"/devices/{driver.device_id}/drivers/{driver.driver_identifier}/signals" + ) + assert response.is_success, response.text + queried_signals = TypeAdapter(list[CarlosDeviceSignal]).validate_json( + response.content + ) + assert len(queried_signals) == len(driver_signals), "All signals should be present" diff --git a/services/api/carlos/api/routes/signals_routes_test.py b/services/api/carlos/api/routes/signals_routes_test.py new file mode 100644 index 00000000..0c3b087f --- /dev/null +++ b/services/api/carlos/api/routes/signals_routes_test.py @@ -0,0 +1,25 @@ +from carlos.database.device import CarlosDeviceSignal, CarlosDeviceSignalUpdate +from carlos.edge.interface.units import UnitOfMeasurement +from starlette.testclient import TestClient + + +def test_update_device_signal_route( + client: TestClient, driver_signals: list[CarlosDeviceSignal] +): + + to_update = CarlosDeviceSignalUpdate( + display_name="Temperature Fahrenheit", + unit_of_measurement=UnitOfMeasurement.FAHRENHEIT, + is_visible_on_dashboard=False, + ) + + response = client.put( + f"/signals/{driver_signals[0].timeseries_id}", + json=to_update.dict(), + ) + assert response.is_success, response.text + updated = CarlosDeviceSignal.model_validate(response.json()) + + assert updated.display_name == to_update.display_name + assert updated.unit_of_measurement == to_update.unit_of_measurement + assert updated.is_visible_on_dashboard == to_update.is_visible_on_dashboard diff --git a/services/api/pyproject.toml b/services/api/pyproject.toml index f7e1aae1..fbf5767b 100644 --- a/services/api/pyproject.toml +++ b/services/api/pyproject.toml @@ -52,6 +52,9 @@ filename = "pyproject.toml" search = "version = \"{current_version}\"" replace = "version = \"{new_version}\"" +[tool.pytest.ini_options] +asyncio_mode = 'auto' + [tool.isort] multi_line_output = 3 include_trailing_comma = true