Skip to content

Commit ef1fc36

Browse files
authored
Add method to associate sensor metadata with measurements (#5)
and fix wrong datetime format string
1 parent 7563590 commit ef1fc36

File tree

4 files changed

+127
-9
lines changed

4 files changed

+127
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def get_recent_measurements(sensebox_id: str):
2424
client = OpenSenseMapClient()
2525
box = await client.get_sensebox(sensebox_id)
2626
sensor_tasks: list[Awaitable[list[Measurement]]] = [
27-
client.get_measurements(box.id, sensor.id) for sensor in box.sensors
27+
client.get_sensor_measurements(box.id, sensor.id) for sensor in box.sensors
2828
]
2929
measurement_series = await asyncio.gather(*sensor_tasks)
3030
await client.close_session()

src/osemclient/client.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""
22
This a docstring for the module.
33
"""
4+
import asyncio
45
import logging
56
from datetime import datetime, timezone
6-
from typing import Optional
7+
from typing import Awaitable, Optional
78

89
from aiohttp import ClientSession, TCPConnector
910
from yarl import URL
1011

11-
from osemclient.models import Box, Measurement, _Measurements
12+
from osemclient.models import Box, Measurement, MeasurementWithSensorMetadata, _Measurements
1213

1314
_logger = logging.getLogger(__name__)
1415

@@ -17,7 +18,7 @@
1718

1819
def _to_osem_dateformat(dt: datetime) -> str:
1920
# OSeM needs the post-decimal places in the ISO/RFC3339 format
20-
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%fZ")
21+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
2122

2223

2324
class OpenSenseMapClient:
@@ -43,7 +44,7 @@ async def get_sensebox(self, sensebox_id: str) -> Box:
4344
_logger.debug("Retrieved sensebox %s", sensebox_id)
4445
return result
4546

46-
async def get_measurements(
47+
async def get_sensor_measurements(
4748
self, sensebox_id: str, sensor_id: str, from_date: Optional[datetime] = None, to_date: Optional[datetime] = None
4849
) -> list[Measurement]:
4950
"""
@@ -67,6 +68,31 @@ async def get_measurements(
6768
)
6869
return results.root
6970

71+
async def get_measurements_with_sensor_metadata(
72+
self, sensebox_id: str, from_date: Optional[datetime] = None, to_date: Optional[datetime] = None
73+
) -> list[MeasurementWithSensorMetadata]:
74+
"""
75+
Returns all the box measurements in the given time range (or the APIs default if not specified).
76+
Other than the get_sensor_measurements method, to use this method you don't have to specify the sensor id.
77+
Also, the return values are annotated with the phenomenon measured.
78+
The result is not sorted in a specific way.
79+
"""
80+
box = await self.get_sensebox(sensebox_id=sensebox_id)
81+
sensor_tasks: list[Awaitable[list[Measurement]]] = [
82+
self.get_sensor_measurements(box.id, sensor.id, from_date=from_date, to_date=to_date)
83+
for sensor in box.sensors
84+
]
85+
sensor_measurements = await asyncio.gather(*sensor_tasks)
86+
_logger.debug("Retrieved %i measurement series for sensors of box %s", len(sensor_measurements), sensebox_id)
87+
results = [
88+
MeasurementWithSensorMetadata.model_validate(
89+
{**sensor.dict(by_alias=True), **measurement.dict(by_alias=True)}
90+
)
91+
for sensor, sensor_measurement_list in zip(box.sensors, sensor_measurements)
92+
for measurement in sensor_measurement_list
93+
]
94+
return results
95+
7096
async def close_session(self):
7197
"""
7298
closes the client session

src/osemclient/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ class _Measurements(RootModel[list[Measurement]]): # pylint:disable=too-few-pub
4949
The array that is returned by
5050
https://api.opensensemap.org/boxes/:senseBoxId/data/:sensorId?from-date=fromDate&to-date=toDate
5151
"""
52+
53+
54+
class MeasurementWithSensorMetadata(Measurement, SensorMetadata):
55+
"""
56+
A measurement with the associated sensor metadata.
57+
"""

unittests/test_client.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ async def test_get_sensebox(self):
142142
assert sensebox.id == "621f53cdb527de001b06ad5e"
143143
assert len(sensebox.sensors) == 11
144144

145-
async def test_get_data_from_sensor(self):
145+
async def test_get_sensor_measurements(self):
146146
with aioresponses() as mocked_api:
147147
mocked_api.get(
148148
# pylint:disable=line-too-long
149-
"https://api.opensensemap.org/boxes/621f53cdb527de001b06ad5e/data/621f53cdb527de001b06ad69?format=json&from-date=2023-12-15T08-00-00.000000Z&to-date=2023-12-15T08-05-00.000000Z",
149+
"https://api.opensensemap.org/boxes/621f53cdb527de001b06ad5e/data/621f53cdb527de001b06ad69?format=json&from-date=2023-12-15T08:00:00.000000Z&to-date=2023-12-15T08:05:00.000000Z",
150150
status=200,
151151
payload=[
152152
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:04:42.215Z", "value": "2.63"},
@@ -156,12 +156,98 @@ async def test_get_data_from_sensor(self):
156156
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:00:36.615Z", "value": "2.68"},
157157
],
158158
)
159-
160159
client = OpenSenseMapClient()
161-
measurements = await client.get_measurements(
160+
measurements = await client.get_sensor_measurements(
162161
"621f53cdb527de001b06ad5e",
163162
"621f53cdb527de001b06ad69",
164163
from_date=_berlin.localize(datetime(2023, 12, 15, 9, 0, 0, 0)),
165164
to_date=_berlin.localize(datetime(2023, 12, 15, 9, 5, 0, 0)),
166165
)
167166
assert len(measurements) == 5
167+
168+
async def test_get_measurements_with_sensor_metadata(self):
169+
with aioresponses() as mocked_api:
170+
mocked_api.get(
171+
"https://api.opensensemap.org/boxes/621f53cdb527de001b06ad5e",
172+
status=200,
173+
payload={
174+
"_id": "621f53cdb527de001b06ad5e",
175+
"createdAt": "2022-03-02T11:23:57.505Z",
176+
"updatedAt": "2023-12-18T13:06:48.041Z",
177+
"name": "CampusJahnallee",
178+
"currentLocation": {
179+
"timestamp": "2022-03-02T11:23:57.500Z",
180+
"coordinates": [12.353332, 51.340222],
181+
"type": "Point",
182+
},
183+
"exposure": "outdoor",
184+
"sensors": [
185+
{
186+
"title": "Temperatur",
187+
"unit": "°C",
188+
"sensorType": "HDC1080",
189+
"icon": "osem-thermometer",
190+
"_id": "621f53cdb527de001b06ad69",
191+
"lastMeasurement": {"createdAt": "2023-12-18T13:06:48.018Z", "value": "9.65"},
192+
},
193+
{
194+
"title": "rel. Luftfeuchte",
195+
"unit": "%",
196+
"sensorType": "HDC1080",
197+
"icon": "osem-humidity",
198+
"_id": "621f53cdb527de001b06ad68",
199+
"lastMeasurement": {"createdAt": "2023-12-18T13:06:48.018Z", "value": "74.66"},
200+
},
201+
# other sensors omitted for brevity
202+
],
203+
"model": "homeV2EthernetFeinstaub",
204+
"lastMeasurementAt": "2023-12-18T13:06:48.018Z",
205+
"description": "SenseBox der GSD Sachunterricht unter besonderer Berücksichtigung von ...",
206+
"image": "621f53cdb527de001b06ad5e_rrlwm9.jpg",
207+
"weblink": "https://www.erzwiss.uni-leipzig.de/institut-fuer-paedagogik-und-didaktik-im-...",
208+
"loc": [
209+
{
210+
"geometry": {
211+
"timestamp": "2022-03-02T11:23:57.500Z",
212+
"coordinates": [12.353332, 51.340222],
213+
"type": "Point",
214+
},
215+
"type": "Feature",
216+
}
217+
],
218+
},
219+
)
220+
mocked_api.get(
221+
# pylint:disable=line-too-long
222+
"https://api.opensensemap.org/boxes/621f53cdb527de001b06ad5e/data/621f53cdb527de001b06ad69?format=json&from-date=2023-12-15T08:00:00.000000Z&to-date=2023-12-15T08:05:00.000000Z",
223+
status=200,
224+
payload=[
225+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:04:42.215Z", "value": "2.63"},
226+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:03:40.855Z", "value": "2.61"},
227+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:02:39.403Z", "value": "2.61"},
228+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:01:37.957Z", "value": "2.63"},
229+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:00:36.615Z", "value": "2.68"},
230+
],
231+
)
232+
mocked_api.get(
233+
# pylint:disable=line-too-long
234+
"https://api.opensensemap.org/boxes/621f53cdb527de001b06ad5e/data/621f53cdb527de001b06ad68?format=json&from-date=2023-12-15T08:00:00.000000Z&to-date=2023-12-15T08:05:00.000000Z",
235+
status=200,
236+
payload=[
237+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:04:42.215Z", "value": "99.51"},
238+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:03:40.855Z", "value": "99.61"},
239+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:02:39.403Z", "value": "99.65"},
240+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:01:37.957Z", "value": "99.71"},
241+
{"location": [12.353332, 51.340222], "createdAt": "2023-12-15T08:00:36.615Z", "value": "99.92"},
242+
],
243+
)
244+
client = OpenSenseMapClient()
245+
results = await client.get_measurements_with_sensor_metadata(
246+
"621f53cdb527de001b06ad5e",
247+
from_date=_berlin.localize(datetime(2023, 12, 15, 9, 0, 0, 0)),
248+
to_date=_berlin.localize(datetime(2023, 12, 15, 9, 5, 0, 0)),
249+
)
250+
assert len(results) == 10
251+
# assert the correct sensors are associated with their respective data
252+
assert all(3 > float(x.value) > 2 for x in results if x.unit == "°C")
253+
assert all(100 > float(x.value) > 99 for x in results if x.unit == "%")

0 commit comments

Comments
 (0)