diff --git a/docs/source/Resources/Devices/Devices_Type.rst b/docs/source/Resources/Devices/Devices_Type.rst index be245e5..0d519c6 100644 --- a/docs/source/Resources/Devices/Devices_Type.rst +++ b/docs/source/Resources/Devices/Devices_Type.rst @@ -559,3 +559,43 @@ DeviceDataRestore | **file_address**: str | File path in TagoIO Files to restore data from (CSV format). + +.. _DeviceStatisticsQuery: + +DeviceStatisticsQuery +--------------------- + + Query parameters for device statistics. All fields are optional. + + **Attributes:** + + | **periodicity**: Literal["hour", "day", "month"] + | Interval between items (default: "hour"). Also sets the default range and the longest range allowed: 1 month for "hour", 3 months for "day", and 12 months for "month". + + | **start_date**: Union[str, datetime] + | Start of the range. Send it together with end_date. If either one is missing, the default range for periodicity is used: the last 24 hours for "hour", the last 3 months for "day", and the last 12 months for "month". + + | **end_date**: Union[str, datetime] + | End of the range. Must not be before start_date. + + | **timezone**: str + | IANA timezone used to read start_date and end_date as local times (default: "UTC"). Send the dates as strings without a UTC offset when you set it. + + +.. _DeviceStatistic: + +DeviceStatistic +--------------- + + Counters of a device at one point in time. + + **Attributes:** + + | **time**: datetime + | When the counters were recorded. + + | [Optional] **device_input**: int or float + | Data records added to the device since the start of the month. Missing when there was no input this month. + + | [Optional] **device_output**: int or float + | Data records returned by the device since the start of the month. Missing when there was no output this month. diff --git a/docs/source/Resources/Devices/index.rst b/docs/source/Resources/Devices/index.rst index cf019d5..9c4afb1 100644 --- a/docs/source/Resources/Devices/index.rst +++ b/docs/source/Resources/Devices/index.rst @@ -601,6 +601,45 @@ Sends data to a device. Accepts a single data object or an array of data objects print(result) # Successfully Inserted +========== +statistics +========== + +Retrieves the data input and output counters of a device over time. +Requires the Control Tower add-on. Counters are month-to-date totals that reset at the start of each calendar month (UTC). +When the range includes the current time, the first item holds the live counters. + + **See:** + + https://docs.tago.io/docs/api/device-statistics Device Statistics + + https://docs.tago.io/docs/tagoio/addons/control-tower Control Tower + + **Parameters:** + + | **deviceID**: :ref:`GenericID` + | Device ID + + | *Optional* **queryParams**: :ref:`DeviceStatisticsQuery` + | Periodicity, date range, and timezone + + **Returns:** + + | list[:ref:`DeviceStatistic`] + +.. code-block:: + :caption: **Example:** + + # If receive an error "Authorization Denied", check policy **Device** / **Access** in Access Management. + resources = Resources() + statistics = resources.devices.statistics("device-id-123", { + "periodicity": "day", + "start_date": "2026-09-01", + "end_date": "2026-09-30", + }) + print(statistics) # [{'time': datetime.datetime(2026, 9, 1, 0, 0), 'device_input': 1250, 'device_output': 8730}, ...] + + =========== tokenCreate =========== diff --git a/pyproject.toml b/pyproject.toml index ab5b47f..60f5c52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "tagoio-sdk" -version = "5.1.7" +version = "5.1.8" description = "Official Python SDK for TagoIO" authors = [{name = "TagoIO Inc.", email = "contact@tago.io"}] license = {text = "Apache-2.0"} diff --git a/src/tagoio_sdk/modules/Resources/Device_Type.py b/src/tagoio_sdk/modules/Resources/Device_Type.py index 5a96e9c..83562f7 100644 --- a/src/tagoio_sdk/modules/Resources/Device_Type.py +++ b/src/tagoio_sdk/modules/Resources/Device_Type.py @@ -649,3 +649,41 @@ class DeviceDataRestore(TypedDict): """ File path in TagoIO Files to restore data from (CSV format). """ + + +class DeviceStatisticsQuery(TypedDict, total=False): + periodicity: Literal["hour", "day", "month"] + """ + Interval between items (default: "hour"). Also sets the default range and the longest range allowed: + 1 month for "hour", 3 months for "day", and 12 months for "month". + """ + start_date: Union[str, datetime] + """ + Start of the range. Send it together with end_date. If either one is missing, the default range + for periodicity is used: the last 24 hours for "hour", the last 3 months for "day", + and the last 12 months for "month". + """ + end_date: Union[str, datetime] + """ + End of the range. Must not be before start_date. + """ + timezone: str + """ + IANA timezone used to read start_date and end_date as local times (default: "UTC"). + Send the dates as strings without a UTC offset when you set it. + """ + + +class DeviceStatistic(TypedDict, total=False): + time: datetime + """ + When the counters were recorded. + """ + device_input: Union[int, float] + """ + Data records added to the device since the start of the month. Missing when there was no input this month. + """ + device_output: Union[int, float] + """ + Data records returned by the device since the start of the month. Missing when there was no output this month. + """ diff --git a/src/tagoio_sdk/modules/Resources/Devices.py b/src/tagoio_sdk/modules/Resources/Devices.py index f924b7b..cc4528f 100644 --- a/src/tagoio_sdk/modules/Resources/Devices.py +++ b/src/tagoio_sdk/modules/Resources/Devices.py @@ -22,6 +22,8 @@ from tagoio_sdk.modules.Resources.Device_Type import DeviceEmptyParams from tagoio_sdk.modules.Resources.Device_Type import DeviceListItem from tagoio_sdk.modules.Resources.Device_Type import DeviceQuery +from tagoio_sdk.modules.Resources.Device_Type import DeviceStatistic +from tagoio_sdk.modules.Resources.Device_Type import DeviceStatisticsQuery from tagoio_sdk.modules.Resources.Device_Type import DeviceTokenDataList from tagoio_sdk.modules.Resources.Device_Type import ListDeviceTokenQuery from tagoio_sdk.modules.Resources.Device_Type import TokenData @@ -611,6 +613,43 @@ def amount(self, deviceID: GenericID) -> Union[int, float]: ) return result + def statistics( + self, deviceID: GenericID, queryParams: Optional[DeviceStatisticsQuery] = None + ) -> List[DeviceStatistic]: + """ + @description: + Retrieves the data input and output counters of a device over time. + Requires the Control Tower add-on. Counters are month-to-date totals that reset at the start of each + calendar month (UTC). When the range includes the current time, the first item holds the live counters. + + @see: + https://docs.tago.io/docs/api/device-statistics Device Statistics + https://docs.tago.io/docs/tagoio/addons/control-tower Control Tower + + @example: + If receive an error "Authorization Denied", check policy **Device** / **Access** in Access Management. + ```python + resources = Resources() + statistics = resources.devices.statistics("device-id-123", { + "periodicity": "day", + "start_date": "2026-09-01", + "end_date": "2026-09-30", + }) + print(statistics) # [{'time': datetime.datetime(2026, 9, 1, 0, 0), 'device_input': 1250, 'device_output': 8730}, ...] + ``` + """ + result = self.doRequest( + { + "path": f"/device/{deviceID}/statistics", + "method": "GET", + "params": queryParams or {}, + } + ) + + result = dateParserList(result, ["time"]) + + return result + def getChunk(self, deviceID: GenericID) -> List[DeviceChunkData]: """ @description: diff --git a/tests/Resources/test_devices.py b/tests/Resources/test_devices.py index fbbdca7..853867a 100644 --- a/tests/Resources/test_devices.py +++ b/tests/Resources/test_devices.py @@ -1,5 +1,6 @@ import os +from datetime import datetime from typing import Any from typing import Dict @@ -423,6 +424,62 @@ def testGetChunk(requests_mock: Mocker) -> None: assert result[0]["amount"] == 1000 +def testStatistics(requests_mock: Mocker) -> None: + """Test statistics method of Devices class.""" + device_id = "device-id-123" + requests_mock.get( + f"https://api.tago.io/device/{device_id}/statistics", + json={ + "status": True, + "result": [ + { + "time": "2026-09-01T00:00:00.000Z", + "device_input": 1250, + "device_output": 8730, + }, + {"time": "2026-09-02T00:00:00.000Z", "device_input": 2480}, + ], + }, + ) + + resources = Resources({"token": "your_token_value"}) + result = resources.devices.statistics( + device_id, + { + "periodicity": "day", + "start_date": "2026-09-01", + "end_date": "2026-09-30", + "timezone": "America/Sao_Paulo", + }, + ) + + assert requests_mock.last_request.qs == { + "periodicity": ["day"], + "start_date": ["2026-09-01"], + "end_date": ["2026-09-30"], + "timezone": ["america/sao_paulo"], + } + assert result[0]["time"] == datetime(2026, 9, 1) + assert result[0]["device_input"] == 1250 + assert result[0]["device_output"] == 8730 + assert "device_output" not in result[1] + + +def testStatisticsWithoutQuery(requests_mock: Mocker) -> None: + """Test statistics method of Devices class without query params.""" + device_id = "device-id-123" + requests_mock.get( + f"https://api.tago.io/device/{device_id}/statistics", + json={"status": True, "result": []}, + ) + + resources = Resources({"token": "your_token_value"}) + result = resources.devices.statistics(device_id) + + assert requests_mock.last_request.qs == {} + assert result == [] + + def testDeleteChunk(requests_mock: Mocker) -> None: """Test deleteChunk method of Devices class.""" device_id = "device-id-123" diff --git a/uv.lock b/uv.lock index 495e923..7da315a 100644 --- a/uv.lock +++ b/uv.lock @@ -421,7 +421,7 @@ wheels = [ [[package]] name = "tagoio-sdk" -version = "5.1.7" +version = "5.1.8" source = { editable = "." } dependencies = [ { name = "python-dateutil" },