async-httpd-data-collector 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ahttpdc/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1"
File without changes
File without changes
@@ -0,0 +1,194 @@
1
+ """
2
+ Module asyncronously fetches the data from given device and writes the readings
3
+ into InfluxDB as records.
4
+
5
+ Author: Piotr Krzysztof Lis - github.com/straightchlorine
6
+ """
7
+
8
+ import asyncio
9
+ import datetime
10
+
11
+ import aiohttp
12
+ from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
13
+ from influxdb_client.client.write.point import Point
14
+
15
+ __all__ = ["AsyncReadFetcher"]
16
+
17
+
18
+ class AsyncReadFetcher:
19
+ """
20
+ Manage fetching the readings from the sensor asyncronously via aiohttp and
21
+ handling received data so that it can be stored within InfluxDB.
22
+
23
+ Attributes:
24
+ _influxdb_host (str): The host of the InfluxDB instance.
25
+ _influxdb_port (int): The port of the InfluxDB instance.
26
+ _influxdb_token (str): The token to authenticate with InfluxDB.
27
+ _influxdb_organization (str): The organization to use within InfluxDB.
28
+ _influxdb_bucket (str): Bucket within InfluxDB where the data will be stored.
29
+ _device_ip (str): The IP address of device providing sensor readings.
30
+ _device_port (str): The port of the device providing the readings.
31
+ _http_handle (str): The http handle to access the data.
32
+ _device_address (str): The address of the device in the network.
33
+ _database_address (str): The address of the InfluxDB instance.
34
+ _sensors (dict): Sensors attached to the device.
35
+ """
36
+
37
+ _influxdb_host: str # host of the influxdb instance
38
+ _influxdb_port: int # port of the influxdb instance
39
+ _influxdb_token: str # token to authenticate with influxdb
40
+ _influxdb_organization: str # organization to use within influxdb
41
+ _influxdb_bucket: str # bucket to save the data into
42
+
43
+ _dev_ip: str # ip of the device sending the data
44
+ _dev_port: int # port of the device sending the data
45
+ _dev_handle: str # handle to access the data
46
+
47
+ _dev_url: str # address of the device in the network
48
+ _db_url: str # address of the influxdb instance
49
+
50
+ def __init__(
51
+ self, host, port, token, org, bucket, sensors, dev_ip, dev_port, handle=""
52
+ ):
53
+ """
54
+ Initialize the fetcher with the required information.
55
+
56
+ Args:
57
+ host (str): Host of the InfluxDB instance.
58
+ port (int): Port of the InfluxDB instance.
59
+ token (str): Token to authenticate with InfluxDB.
60
+ org (str): Organization to use within InfluxDB.
61
+ bucket (str): Bucket within InfluxDB where the data will be stored.
62
+ dev_ip (str):IP address of device providing sensor readings.
63
+ dev_port (str): Port of the device providing the readings.
64
+ handle (str): Http handle to access the data ("" by default).
65
+ sensors (dict): Which sensors device has and what do they measure.
66
+ """
67
+
68
+ # InfluxDB authentication data
69
+ self._influxdb_host = host
70
+ self._influxdb_port = port
71
+ self._influxdb_token = token
72
+ self._influxdb_organization = org
73
+ self._influxdb_bucket = bucket
74
+
75
+ # device identification
76
+ self._dev_ip = dev_ip
77
+ self._dev_port = dev_port
78
+ self._dev_handle = handle
79
+
80
+ # device and database URLs
81
+ self._dev_url = f"http://{self._dev_ip}:{self._dev_port}/{self._dev_handle}"
82
+ self._db_url = f"http://{self._influxdb_host}:{self._influxdb_port}"
83
+
84
+ self._sensors = sensors
85
+
86
+ def _get_reads(self, data) -> dict[str, float]:
87
+ """
88
+ Based on sensors specified in sensors attribute fill the fields
89
+ with appropriate key-value pairs for InfluxDB storage.
90
+
91
+ Args:
92
+ data (dict): The sensor readings to parse.
93
+ """
94
+
95
+ fields = {}
96
+ for sensor in self._sensors:
97
+ for param in self._sensors[sensor]:
98
+ fields[param] = float(data["nodemcu"][sensor][param])
99
+ return fields
100
+
101
+ def _parse_into_records(self, data, device_name="nodemcu"):
102
+ """
103
+ Parse raw json file into records for InfluxDB.
104
+
105
+ Args:
106
+ data (dict): The sensor readings to parse.
107
+ device_name (str): The name of the device (default is 'nodemcu').
108
+ """
109
+
110
+ records = {
111
+ "measurement": "sensor_data",
112
+ "tags": {"device": device_name},
113
+ "timestamp": str(datetime.datetime.now()),
114
+ }
115
+
116
+ records["fields"] = self._get_reads(data)
117
+ return records
118
+
119
+ async def _write_to_db(self, client, record):
120
+ """
121
+ Write the sensor readings to InfluxDB.
122
+
123
+ Args:
124
+ client (InfluxDBClientAsync): The InfluxDB client to write to.
125
+ records (dict): The sensor readings as records for InfluxDB.
126
+ """
127
+
128
+ print("<.> writing new read into database...")
129
+ write_api = client.write_api()
130
+ point = Point.from_dict(record, write_precision="ns")
131
+ await write_api.write(
132
+ bucket=self._influxdb_bucket, org=self._influxdb_organization, record=point
133
+ )
134
+
135
+ async def _store_sensor_readings(self, record):
136
+ """
137
+ Store sensor readings within InfluxDB.
138
+
139
+ Args:
140
+ records (dict): The sensor readings in the form of InfluxDB records.
141
+ """
142
+
143
+ async with InfluxDBClientAsync(
144
+ url=self._db_url,
145
+ token=self._influxdb_token,
146
+ org=self._influxdb_organization,
147
+ ) as client:
148
+ await self._write_to_db(client, record)
149
+
150
+ async def _request_sensor_readings(self, session):
151
+ """
152
+ Fetch the sensor readings from the device via http request.
153
+
154
+ Args:
155
+ session: The aiohttp session to use for the request.
156
+ """
157
+
158
+ async with session.get(self._dev_url) as response:
159
+ if response.status != 200:
160
+ print(f"Error fetching data: {response.status}")
161
+ else:
162
+ read = await response.json()
163
+ return read
164
+
165
+ async def _request_and_store(self):
166
+ """
167
+ Request sensor reading via aiohttp and store them in InfluxDB.
168
+ """
169
+
170
+ async with aiohttp.ClientSession() as session:
171
+ json = await self._request_sensor_readings(session)
172
+ await self._store_sensor_readings(self._parse_into_records(json))
173
+
174
+ async def _fetching_loop(self):
175
+ """
176
+ Main fetcher loop to request and store sensor readings.
177
+
178
+ Loop is infinite and receives readings every second, meant to run in
179
+ the background.
180
+ """
181
+
182
+ while True:
183
+ await asyncio.sleep(1)
184
+ await self._request_and_store()
185
+
186
+ async def schedule_fetcher(self):
187
+ """
188
+ Create a task group managing the fetching loop.
189
+
190
+ Tested using asyncio.run()
191
+ """
192
+
193
+ async with asyncio.TaskGroup() as tg:
194
+ await tg.create_task(self._fetching_loop())
@@ -0,0 +1,168 @@
1
+ """
2
+ Interface that control both fetching, writing and querying from the database.
3
+
4
+ Author: Piotr Krzysztof Lis - github.com/straightchlorine
5
+ """
6
+
7
+ import asyncio
8
+ import multiprocessing
9
+
10
+ import pandas as pd
11
+
12
+ from ahttpdc.reads.fetch.async_fetch import AsyncReadFetcher
13
+ from ahttpdc.reads.query.async_query import AsyncQuery
14
+
15
+ __all__ = ["DatabaseInterface"]
16
+
17
+
18
+ class DatabaseInterface:
19
+ """
20
+ Interface to control fetching, writing and querying from the database.
21
+
22
+ Attributes:
23
+ _influxdb_host (str): The host of the InfluxDB instance.
24
+ _influxdb_port (int): The port of the InfluxDB instance.
25
+ _influxdb_token (str): The token to authenticate with InfluxDB.
26
+ _influxdb_organization (str): The organization to use within InfluxDB.
27
+ _influxdb_bucket (str): Bucket within InfluxDB where the data will be stored.
28
+ _dev_ip (str): The IP address of device providing sensor readings.
29
+ _dev_port (str): The port of the device providing the readings.
30
+ _dev_handle (str): The http handle to access the data.
31
+ _dev_url (str): The address of the device in the network.
32
+ _db_url (str): The address of the influxdb instance.
33
+ sensors (dict): The sensors and their parameters to read.
34
+ """
35
+
36
+ _influxdb_host: str # host of the influxdb instance
37
+ _influxdb_port: int # port of the influxdb instance
38
+ _influxdb_token: str # token to authenticate with influxdb
39
+ _influxdb_organization: str # organization to use within influxdb
40
+ _influxdb_bucket: str # bucket to save the data into
41
+
42
+ _dev_ip: str # ip of the device sending the data
43
+ _dev_port: int # port of the device sending the data
44
+ _dev_handle: str # handle to access the data
45
+
46
+ _dev_url: str # address of the device in the network
47
+ _db_url: str # address of the influxdb instance
48
+
49
+ def __init__(
50
+ self, host, port, token, org, bucket, sensors, dev_ip, dev_port, handle=""
51
+ ):
52
+ """
53
+ Initialize the fetcher with the required information.
54
+
55
+ Args:
56
+ host (str): The host of the InfluxDB instance.
57
+ port (int): The port of the InfluxDB instance.
58
+ token (str): The token to authenticate with InfluxDB.
59
+ org (str): The organization to use within InfluxDB.
60
+ bucket (str): Bucket within InfluxDB where the data will be stored.
61
+ dev_ip (str): The IP address of device providing sensor readings.
62
+ dev_port (str): The port of the device providing the readings.
63
+ handle (str): The http handle to access the data ("" by default).
64
+ sensors (dict): The sensors and their parameters to read.
65
+ """
66
+
67
+ self._influxdb_host = host
68
+ self._influxdb_port = port
69
+ self._influxdb_token = token
70
+ self._influxdb_organization = org
71
+ self._influxdb_bucket = bucket
72
+
73
+ self._dev_ip = dev_ip
74
+ self._dev_port = dev_port
75
+ self._dev_handle = handle
76
+
77
+ self._dev_url = f"http://{self._dev_ip}:{self._dev_port}/{self._dev_handle}"
78
+ self._db_url = f"http://{self._influxdb_host}:{self._influxdb_port}"
79
+
80
+ self.sensors = sensors
81
+
82
+ self._fetcher = AsyncReadFetcher(
83
+ self._influxdb_host,
84
+ self._influxdb_port,
85
+ self._influxdb_token,
86
+ self._influxdb_organization,
87
+ self._influxdb_bucket,
88
+ self.sensors,
89
+ self._dev_ip,
90
+ self._dev_port,
91
+ self._dev_handle,
92
+ )
93
+
94
+ self.query_interface = AsyncQuery(
95
+ self._influxdb_host,
96
+ self._influxdb_port,
97
+ self._influxdb_token,
98
+ self._influxdb_organization,
99
+ self._influxdb_bucket,
100
+ self.sensors,
101
+ )
102
+
103
+ def enable_fetching(self):
104
+ """
105
+ Enable fetching from the device specified by dev_ip, dev_port and handle.
106
+
107
+ Starts the fetching task in the background, thus should be invoked last
108
+ in order to avoid blocking the main thread.
109
+ """
110
+
111
+ def _start_fetching():
112
+ asyncio.run(self._fetcher.schedule_fetcher())
113
+
114
+ fetching_process = multiprocessing.Process(
115
+ target=_start_fetching, name="asyncfetcher"
116
+ )
117
+ fetching_process.start()
118
+
119
+ async def query_latest(self) -> pd.DataFrame:
120
+ """
121
+ Query the latest measurement from the InfluxDB.
122
+
123
+ Returns:
124
+ pd.DataFrame: The latest measurement.
125
+ """
126
+
127
+ print("<.> querying latest measurement...")
128
+ query_task = asyncio.create_task(self.query_interface.latest())
129
+ await query_task
130
+ result = query_task.result()
131
+ return result
132
+
133
+ async def query_historical(self, start: str, end: str) -> pd.DataFrame:
134
+ """
135
+ Query historical data from the database.
136
+
137
+ Args:
138
+ start (str): Start time of the query (e.g., '2024-01-01T00:00:00Z').
139
+ end (str): End time of the query (e.g., '2024-01-02T00:00:00Z').
140
+
141
+ Returns:
142
+ pd.DataFrame: Historical data within the specified time range.
143
+ """
144
+
145
+ print("<.> querying historical data...")
146
+ query_task = asyncio.create_task(
147
+ self.query_interface.historical_data(start, end)
148
+ )
149
+ await query_task
150
+ result = query_task.result()
151
+ return result
152
+
153
+ async def query(self, query: str) -> pd.DataFrame:
154
+ """
155
+ Perform a custom query on the database.
156
+
157
+ Args:
158
+ query (str): The InfluxDB query to execute.
159
+
160
+ Returns:
161
+ pd.DataFrame: The result of the custom query.
162
+ """
163
+
164
+ print("<.> custom query...")
165
+ query_task = asyncio.create_task(self.query_interface.query(query))
166
+ await query_task
167
+ result = query_task.result()
168
+ return result
File without changes
@@ -0,0 +1,222 @@
1
+ """
2
+ Interface for passing queries to the database.
3
+
4
+ Author: Piotr Krzysztof Lis - github.com/straightchlorine
5
+ """
6
+
7
+ from datetime import datetime, timedelta
8
+
9
+ from influxdb_client.client.exceptions import InfluxDBError
10
+ from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
11
+ import pandas as pd
12
+
13
+ __all__ = ["AsyncQuery"]
14
+
15
+
16
+ class AsyncQuery:
17
+ """
18
+ Interface to query the InfluxDB.
19
+
20
+ Attributes:
21
+ _influxdb_host (str): The host of the InfluxDB instance.
22
+ _influxdb_port (int): The port of the InfluxDB instance.
23
+ _influxdb_token (str): The token to authenticate with InfluxDB.
24
+ _influxdb_organization (str): The organization to use within InfluxDB.
25
+ _influxdb_bucket (str): Bucket within InfluxDB where the data will be stored.
26
+ _db_url (str): The URL of the InfluxDB instance.
27
+ sensors_and_params (dict): The sensors and their parameters to read.
28
+ """
29
+
30
+ _influxdb_host: str # host of the influxdb instance
31
+ _influxdb_port: int # port of the influxdb instance
32
+ _influxdb_token: str # token to authenticate with influxdb
33
+ _influxdb_organization: str # organization to use within influxdb
34
+ _influxdb_bucket: str # bucket to save the data into
35
+
36
+ _db_url: str # address of the influxdb instance
37
+
38
+ sensors: dict # sensors and their parameters to read
39
+
40
+ def __init__(self, host, port, token, org, bucket, sensors):
41
+ """
42
+ Initialize the fetcher with the required information.
43
+
44
+ Args:
45
+ host (str): The host of the InfluxDB instance.
46
+ port (int): The port of the InfluxDB instance.
47
+ token (str): The token to authenticate with InfluxDB.
48
+ org (str): The organization to use within InfluxDB.
49
+ bucket (str): Bucket within InfluxDB where the data will be stored.
50
+ sensors (dict): The sensors and their parameters to read.
51
+ """
52
+
53
+ self._influxdb_host = host
54
+ self._influxdb_port = port
55
+ self._influxdb_token = token
56
+ self._influxdb_organization = org
57
+ self._influxdb_bucket = bucket
58
+
59
+ self._db_url = f"http://{self._influxdb_host}:{self._influxdb_port}"
60
+
61
+ self.sensors = sensors
62
+
63
+ async def _get_InfluxDB_client(self) -> InfluxDBClientAsync:
64
+ """Returns an InfluxDB client."""
65
+ return InfluxDBClientAsync(
66
+ url=self._db_url,
67
+ token=self._influxdb_token,
68
+ org=self._influxdb_organization,
69
+ )
70
+
71
+ def _convert_to_local_time(self, timestamps):
72
+ """
73
+ Convert a collection of UTC timestamps to local time.
74
+
75
+ Args:
76
+ timestamps (list): A list of UTC timestamps.
77
+ Returns:
78
+ list: A list of timestamps in local time.
79
+ """
80
+
81
+ local_timestamps = []
82
+
83
+ # get the local offset from UTC
84
+ local_offset = timedelta(
85
+ seconds=datetime.now().astimezone().utcoffset().total_seconds()
86
+ )
87
+
88
+ # add the offset to the timestamps
89
+ for timestamp in timestamps:
90
+ utc_time = pd.to_datetime(timestamp)
91
+ local_time = utc_time + local_offset
92
+ local_timestamps.append(local_time)
93
+
94
+ return local_timestamps
95
+
96
+ def _into_dataframe(self, tables) -> pd.DataFrame:
97
+ """
98
+ Turns the tables into a pandas DataFrame.
99
+ Args:
100
+ tables (list): The tables to turn into a DataFrame.
101
+ Returns:
102
+ pd.DataFrame: procured measurements as a DataFrame.
103
+ """
104
+
105
+ read: dict = {}
106
+ timestamps = set()
107
+
108
+ # unpacking the table
109
+ for table in tables:
110
+ for record in table.records:
111
+ # get the measurements
112
+ parameter = record.get_field()
113
+ measurement = record.get_value()
114
+ timestamps.add(record.get_time())
115
+
116
+ # ensure every parameter is present in the dict
117
+ if parameter not in read:
118
+ read[parameter] = []
119
+ read[parameter].append(float(measurement))
120
+
121
+ # convert timestamps to local time
122
+ local_timestamps = self._convert_to_local_time(timestamps)
123
+
124
+ # if there is no time key, create one
125
+ if "time" not in read:
126
+ read["time"] = []
127
+
128
+ # add the timestamps to the data dict
129
+ for timestamp in local_timestamps:
130
+ read["time"].append(pd.to_datetime(timestamp))
131
+
132
+ # return the data as a DataFrame
133
+ return pd.DataFrame(read)
134
+
135
+ async def latest(self) -> pd.DataFrame:
136
+ """
137
+ Query the database for the latest measurement.
138
+
139
+ Returns:
140
+ pd.DataFrame: The latest measurement of every parameter.
141
+ """
142
+
143
+ # get the connection to the database via query api
144
+ client = await self._get_InfluxDB_client()
145
+ query_api = client.query_api()
146
+
147
+ # query the latest measurement
148
+ query = f'from(bucket:"{self._influxdb_bucket}") |> range(start: -1h) |> last()'
149
+
150
+ tables = None
151
+ try:
152
+ tables = await query_api.query(query)
153
+ except InfluxDBError as e:
154
+ print(f"Exception caught while querying the database:\n\n {e.message}")
155
+
156
+ # close the connection
157
+ await client.close()
158
+
159
+ # turn the tables into a DataFrame and return it
160
+ if tables is not None:
161
+ return self._into_dataframe(tables)
162
+ else:
163
+ return pd.DataFrame()
164
+
165
+ async def historical_data(self, start: str, end: str) -> pd.DataFrame:
166
+ """
167
+ Query historical data from the database.
168
+
169
+ Args:
170
+ start (str): Start time of the query (e.g., '2024-01-01T00:00:00Z').
171
+ end (str): End time of the query (e.g., '2024-01-02T00:00:00Z').
172
+
173
+ Returns:
174
+ pd.DataFrame: Historical data within the specified time range.
175
+ """
176
+
177
+ # get the connection to the database via query api
178
+ client = await self._get_InfluxDB_client()
179
+ query_api = client.query_api()
180
+
181
+ # query the data from specified start and finish
182
+ query = f'from(bucket:"{self._influxdb_bucket}") |> range(start: {start}, stop: {end})'
183
+
184
+ tables = None
185
+ try:
186
+ tables = await query_api.query(query)
187
+ except InfluxDBError as e:
188
+ print(f"Exception caught while querying the database:\n\n {e.message}")
189
+
190
+ await client.close()
191
+
192
+ if tables is not None:
193
+ return self._into_dataframe(tables)
194
+ else:
195
+ return pd.DataFrame()
196
+
197
+ async def query(self, query: str) -> pd.DataFrame:
198
+ """
199
+ Perform a custom query on the database.
200
+
201
+ Args:
202
+ query (str): The InfluxDB query to execute.
203
+
204
+ Returns:
205
+ pd.DataFrame: The result of the custom query.
206
+ """
207
+ client = await self._get_InfluxDB_client()
208
+ query_api = client.query_api()
209
+
210
+ tables = None
211
+
212
+ try:
213
+ tables = await query_api.query(query)
214
+ except InfluxDBError as e:
215
+ print(f"Exception caught while querying the database:\n\n {e.message}")
216
+
217
+ await client.close()
218
+
219
+ if tables is not None:
220
+ return self._into_dataframe(tables)
221
+ else:
222
+ return pd.DataFrame()