Skip to content

Commit 43fc18f

Browse files
committed
http: api: implement latest telemetry
1 parent 492b0b6 commit 43fc18f

File tree

7 files changed

+140
-1
lines changed

7 files changed

+140
-1
lines changed

src/enapter/cli/http/api/command.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .command_command import CommandCommand
77
from .device_command import DeviceCommand
88
from .site_command import SiteCommand
9+
from .telemetry_command import TelemetryCommand
910

1011

1112
class Command(cli.Command):
@@ -21,6 +22,7 @@ def register(parent: cli.Subparsers) -> None:
2122
CommandCommand,
2223
DeviceCommand,
2324
SiteCommand,
25+
TelemetryCommand,
2426
]:
2527
command.register(subparsers)
2628

@@ -35,5 +37,7 @@ async def run(args: argparse.Namespace) -> None:
3537
await DeviceCommand.run(args)
3638
case "site":
3739
await SiteCommand.run(args)
40+
case "telemetry":
41+
await TelemetryCommand.run(args)
3842
case _:
3943
raise NotImplementedError(args.device_command)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import argparse
2+
3+
from enapter import cli
4+
5+
from .telemetry_latest_command import TelemetryLatestCommand
6+
7+
8+
class TelemetryCommand(cli.Command):
9+
10+
@staticmethod
11+
def register(parent: cli.Subparsers) -> None:
12+
parser = parent.add_parser(
13+
"telemetry", formatter_class=argparse.ArgumentDefaultsHelpFormatter
14+
)
15+
subparsers = parser.add_subparsers(dest="telemetry_command", required=True)
16+
for command in [
17+
TelemetryLatestCommand,
18+
]:
19+
command.register(subparsers)
20+
21+
@staticmethod
22+
async def run(args: argparse.Namespace) -> None:
23+
match args.telemetry_command:
24+
case "latest":
25+
await TelemetryLatestCommand.run(args)
26+
case _:
27+
raise NotImplementedError(args.telemetry_command)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import argparse
2+
import json
3+
4+
from enapter import cli, http
5+
6+
7+
class TelemetryLatestCommand(cli.Command):
8+
9+
@staticmethod
10+
def register(parent: cli.Subparsers) -> None:
11+
parser = parent.add_parser(
12+
"latest", formatter_class=argparse.ArgumentDefaultsHelpFormatter
13+
)
14+
parser.add_argument(
15+
"device_attrs",
16+
metavar="device:attr1,attr2,...,attrN",
17+
nargs="+",
18+
help="Device attributes to get the latest telemetry for",
19+
)
20+
21+
@staticmethod
22+
async def run(args: argparse.Namespace) -> None:
23+
async with http.api.Client(http.api.Config.from_env()) as client:
24+
attributes_by_device = {}
25+
for device_attrs in args.device_attrs:
26+
device, attrs_str = device_attrs.split(":", 1)
27+
attributes_by_device[device] = attrs_str.split(",")
28+
telemetry = await client.telemetry.latest(attributes_by_device)
29+
dto = {
30+
device: {
31+
attribute: datapoint.to_dto() if datapoint is not None else None
32+
for attribute, datapoint in datapoints.items()
33+
}
34+
for device, datapoints in telemetry.items()
35+
}
36+
print(json.dumps(dto))

src/enapter/http/api/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import httpx
44

5-
from enapter.http.api import blueprints, commands, devices, sites
5+
from enapter.http.api import blueprints, commands, devices, sites, telemetry
66

77
from .config import Config
88

@@ -45,3 +45,7 @@ def commands(self) -> commands.Client:
4545
@property
4646
def blueprints(self) -> blueprints.Client:
4747
return blueprints.Client(client=self._client)
48+
49+
@property
50+
def telemetry(self) -> telemetry.Client:
51+
return telemetry.Client(client=self._client)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .client import Client
2+
from .latest_datapoint import LatestDatapoint
3+
4+
__all__ = ["Client", "LatestDatapoint"]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import datetime
2+
3+
import httpx
4+
5+
from enapter.http import api
6+
7+
from .latest_datapoint import LatestDatapoint
8+
9+
10+
class Client:
11+
12+
def __init__(self, client: httpx.AsyncClient) -> None:
13+
self._client = client
14+
15+
async def latest(
16+
self,
17+
attributes_by_device: dict[str, list[str]],
18+
relevance_interval: int | datetime.timedelta | None = None,
19+
) -> dict[str, dict[str, LatestDatapoint | None]]:
20+
url = "v3/telemetry/latest"
21+
params = {
22+
f"devices[{device}]": ",".join(attributes)
23+
for device, attributes in attributes_by_device.items()
24+
}
25+
if relevance_interval is not None:
26+
if isinstance(relevance_interval, datetime.timedelta):
27+
relevance_interval = int(relevance_interval.total_seconds())
28+
params["relevance_interval"] = str(relevance_interval) + "s"
29+
response = await self._client.get(url, params=params)
30+
api.check_error(response)
31+
return {
32+
device: {
33+
attribute: (
34+
LatestDatapoint.from_dto(datapoint)
35+
if datapoint.get("timestamp") is not None
36+
else None
37+
)
38+
for attribute, datapoint in datapoints.items()
39+
}
40+
for device, datapoints in response.json().get("telemetry", {}).items()
41+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import dataclasses
2+
import datetime
3+
from typing import Any, Self
4+
5+
6+
@dataclasses.dataclass
7+
class LatestDatapoint:
8+
9+
timestamp: datetime.datetime
10+
value: Any
11+
12+
@classmethod
13+
def from_dto(cls, dto: dict[str, Any]) -> Self:
14+
return cls(
15+
timestamp=datetime.datetime.fromtimestamp(dto["timestamp"]),
16+
value=dto.get("value"),
17+
)
18+
19+
def to_dto(self) -> dict[str, Any]:
20+
return {
21+
"timestamp": int(self.timestamp.timestamp()),
22+
"value": self.value,
23+
}

0 commit comments

Comments
 (0)