async-httpd-data-collector 2.0.2__tar.gz → 2.0.3__tar.gz

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.
Files changed (41) hide show
  1. async_httpd_data_collector-2.0.3/.github/workflows/docs.yml +40 -0
  2. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/.github/workflows/publish.yml +8 -14
  3. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/.github/workflows/test.yml +6 -6
  4. async_httpd_data_collector-2.0.3/.gitignore +45 -0
  5. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/PKG-INFO +89 -274
  6. async_httpd_data_collector-2.0.3/README.md +115 -0
  7. async_httpd_data_collector-2.0.3/ahttpdc/__init__.py +1 -0
  8. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/daemon.py +3 -3
  9. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/database_interface.py +2 -2
  10. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/fetch/fetcher.py +2 -2
  11. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/query/interface.py +3 -3
  12. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/store/collector.py +3 -3
  13. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/store/parse/parser.py +2 -2
  14. async_httpd_data_collector-2.0.3/docs/analysis/index.md +197 -0
  15. async_httpd_data_collector-2.0.3/docs/analysis/prediction.md +161 -0
  16. async_httpd_data_collector-2.0.3/docs/api.md +231 -0
  17. async_httpd_data_collector-2.0.3/docs/architecture.md +132 -0
  18. async_httpd_data_collector-2.0.3/docs/getting-started.md +163 -0
  19. async_httpd_data_collector-2.0.3/docs/index.md +69 -0
  20. async_httpd_data_collector-2.0.3/mkdocs.yml +38 -0
  21. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/pyproject.toml +8 -34
  22. async_httpd_data_collector-2.0.2/.gitignore +0 -165
  23. async_httpd_data_collector-2.0.2/README.md +0 -274
  24. async_httpd_data_collector-2.0.2/ahttpdc/__init__.py +0 -1
  25. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/.yamlfmt.yml +0 -0
  26. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/.yamllint.yml +0 -0
  27. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/LICENSE +0 -0
  28. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/__init__.py +0 -0
  29. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/fetch/__init__.py +0 -0
  30. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/query/__init__.py +0 -0
  31. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/query/parse/data.py +0 -0
  32. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/store/__init__.py +0 -0
  33. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/ahttpdc/read/store/parse/__init__.py +0 -0
  34. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/examples/daemon_example.py +0 -0
  35. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/examples/minima_dashboard_example.py +0 -0
  36. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/examples/query_example.py +0 -0
  37. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/requirements.txt +0 -0
  38. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/tests/dev_server.py +0 -0
  39. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/tests/read/fetch/test_fetcher.py +0 -0
  40. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/tests/read/query/test_query.py +0 -0
  41. {async_httpd_data_collector-2.0.2 → async_httpd_data_collector-2.0.3}/tests/read/test_interface.py +0 -0
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: Docs
3
+
4
+ on:
5
+ push:
6
+ branches: [master]
7
+
8
+ permissions:
9
+ pages: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ build-docs:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - name: Install mkdocs-material
23
+ run: pip install mkdocs-material
24
+
25
+ - name: Build docs
26
+ run: mkdocs build
27
+
28
+ - uses: actions/upload-pages-artifact@v3
29
+ with:
30
+ path: site/
31
+
32
+ deploy:
33
+ needs: build-docs
34
+ environment:
35
+ name: github-pages
36
+ url: ${{ steps.deployment.outputs.page_url }}
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/deploy-pages@v4
40
+ id: deployment
@@ -1,17 +1,13 @@
1
1
  ---
2
- # Builds, relases and publishes to PyPi
2
+ # Builds, releases and publishes to PyPI
3
3
  # .github/workflows/publish.yml
4
4
  name: CD
5
5
 
6
- # triggered when testing is finished
7
6
  on:
8
- workflow_run:
9
- workflows: ["CI"]
10
- branches: [master]
11
- types:
12
- - completed
7
+ push:
8
+ tags:
9
+ - 'v*'
13
10
 
14
- # add contents: write for the release
15
11
  permissions:
16
12
  contents: write
17
13
 
@@ -23,10 +19,10 @@ jobs:
23
19
  steps:
24
20
 
25
21
  - name: Checkout repository
26
- uses: actions/checkout@v2
22
+ uses: actions/checkout@v4
27
23
 
28
24
  - name: Set up Python
29
- uses: actions/setup-python@v2
25
+ uses: actions/setup-python@v5
30
26
  with:
31
27
  python-version: '3.x'
32
28
 
@@ -34,20 +30,18 @@ jobs:
34
30
  run: |
35
31
  python -m pip install --upgrade pip
36
32
  pip install build twine
37
- pip install -r requirements.txt
38
33
 
39
- - name: Build the application
34
+ - name: Build the package
40
35
  run: |
41
36
  python -m build
42
37
 
43
38
  - name: Release
44
39
  uses: softprops/action-gh-release@v2
45
- if: startsWith(github.ref, 'refs/tags/')
46
40
  with:
47
41
  files: |
48
42
  dist/**
49
43
 
50
- - name: Publish to PyPi
44
+ - name: Publish to PyPI
51
45
  env:
52
46
  TWINE_USERNAME: __token__
53
47
  TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
@@ -28,7 +28,7 @@ jobs:
28
28
  steps:
29
29
 
30
30
  - name: Checkout repository
31
- uses: actions/checkout@v2
31
+ uses: actions/checkout@v4
32
32
 
33
33
  # install InfluxDB CLI to authenticate access to the database
34
34
  - name: Install InfluxDB CLI
@@ -56,8 +56,8 @@ jobs:
56
56
  --force
57
57
 
58
58
  # save the data required for the token generation and testing
59
- echo "::set-output name=org::${INFLUX_ORG}"
60
- echo "::set-output name=bucket::${INFLUX_BUCKET}"
59
+ echo "org=${INFLUX_ORG}" >> $GITHUB_OUTPUT
60
+ echo "bucket=${INFLUX_BUCKET}" >> $GITHUB_OUTPUT
61
61
 
62
62
  - name: Generate token for further authentication
63
63
  id: auth
@@ -72,10 +72,10 @@ jobs:
72
72
  --json | jq '.token')
73
73
 
74
74
  # save the token
75
- echo "::set-output name=token::${INFLUX_TOKEN}"
75
+ echo "token=${INFLUX_TOKEN}" >> $GITHUB_OUTPUT
76
76
 
77
77
  - name: Set up Python
78
- uses: actions/setup-python@v2
78
+ uses: actions/setup-python@v5
79
79
  with:
80
80
  python-version: '3.11'
81
81
 
@@ -92,7 +92,7 @@ jobs:
92
92
  - name: Test
93
93
  run: |-
94
94
 
95
- # run the falsk dev_server
95
+ # run the flask dev_server
96
96
  export FLASK_APP="tests/dev_server"
97
97
  flask run --host=0.0.0.0 --port=9000 &
98
98
  sleep 5
@@ -0,0 +1,45 @@
1
+ # project specific
2
+ secrets
3
+ *env
4
+ test-data
5
+
6
+ # byte-compiled / optimized
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+
12
+ # distribution / packaging
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+ *.egg
17
+ MANIFEST
18
+
19
+ # unit test / coverage
20
+ htmlcov/
21
+ .tox/
22
+ .nox/
23
+ .coverage
24
+ .coverage.*
25
+ .cache
26
+ .pytest_cache/
27
+ coverage.xml
28
+
29
+ # environments
30
+ .env
31
+ .venv
32
+ venv/
33
+
34
+ # mkdocs
35
+ /site
36
+ docs/analysis/img/
37
+
38
+ # mypy
39
+ .mypy_cache/
40
+
41
+ # ruff
42
+ .ruff_cache/
43
+
44
+ # logs
45
+ *.log
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: async-httpd-data-collector
3
- Version: 2.0.2
4
- Summary: Gateway facilitating asyncronous communication between sensory data-emitting devices, InfluxDB and the user.
3
+ Version: 2.0.3
4
+ Summary: Gateway facilitating asynchronous communication between sensory data-emitting devices, InfluxDB and the user.
5
+ Project-URL: Documentation, https://ahttpdc-docs.codextechnologies.org/mkdocs
5
6
  Project-URL: Repository, https://github.com/straightchlorine/async-httpd-data-collector
6
7
  Project-URL: Issues, https://github.com/straightchlorine/async-httpd-data-collector/issues
7
8
  Author-email: Piotr Krzysztof Lis <piotrlis555@gmail.com>
9
+ Maintainer-email: Piotr Krzysztof Lis <piotrlis555@gmail.com>
8
10
  License: GNU GENERAL PUBLIC LICENSE
9
11
  Version 3, 29 June 2007
10
12
 
@@ -689,322 +691,135 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
689
691
  Requires-Python: >=3.8
690
692
  Requires-Dist: aiocsv
691
693
  Requires-Dist: aiohttp
692
- Requires-Dist: aiosignal
693
- Requires-Dist: attrs
694
- Requires-Dist: certifi
695
- Requires-Dist: frozenlist
696
- Requires-Dist: idna
697
694
  Requires-Dist: influxdb-client[async]
698
- Requires-Dist: multidict
699
695
  Requires-Dist: numpy
700
696
  Requires-Dist: pandas
701
697
  Requires-Dist: python-dateutil
702
698
  Requires-Dist: pytz
703
699
  Requires-Dist: reactivex
704
- Requires-Dist: setuptools
705
- Requires-Dist: six
706
- Requires-Dist: typing-extensions
707
- Requires-Dist: tzdata
708
- Requires-Dist: urllib3
709
- Requires-Dist: yarl
700
+ Provides-Extra: docs
701
+ Requires-Dist: mkdocs-material; extra == 'docs'
710
702
  Provides-Extra: test
711
- Requires-Dist: blinker; extra == 'test'
712
- Requires-Dist: charset-normalizer; extra == 'test'
713
- Requires-Dist: click; extra == 'test'
714
703
  Requires-Dist: dash; extra == 'test'
715
- Requires-Dist: dash-core-components; extra == 'test'
716
- Requires-Dist: dash-html-components; extra == 'test'
717
- Requires-Dist: dash-table; extra == 'test'
718
704
  Requires-Dist: flask; extra == 'test'
719
- Requires-Dist: importlib-metadata; extra == 'test'
720
- Requires-Dist: iniconfig; extra == 'test'
721
- Requires-Dist: itsdangerous; extra == 'test'
722
- Requires-Dist: jinja2; extra == 'test'
723
- Requires-Dist: markupsafe; extra == 'test'
724
- Requires-Dist: nest-asyncio; extra == 'test'
725
- Requires-Dist: packaging; extra == 'test'
726
705
  Requires-Dist: plotly; extra == 'test'
727
- Requires-Dist: pluggy; extra == 'test'
728
706
  Requires-Dist: pytest; extra == 'test'
729
707
  Requires-Dist: pytest-asyncio; extra == 'test'
730
708
  Requires-Dist: requests; extra == 'test'
731
- Requires-Dist: retrying; extra == 'test'
732
- Requires-Dist: tenacity; extra == 'test'
733
- Requires-Dist: werkzeug; extra == 'test'
734
- Requires-Dist: zipp; extra == 'test'
735
709
  Description-Content-Type: text/markdown
736
710
 
737
- # async-httpd-data-collector
738
-
739
- Interface handling the communication between sensory data-emitting devices, InfluxDB and the user.
740
-
741
- The most important object that a user would use is `DatabseInterface` within `ahttpdc.reads.interface` module.
742
- This class facilitates the communication between the fetcher and the querying apis of InfluxDB.
711
+ <div align="center">
743
712
 
744
- In order to control fetching, there are two methods:
713
+ [![PyPI version](https://badge.fury.io/py/async-httpd-data-collector.svg)](https://pypi.org/project/async-httpd-data-collector/)
714
+ [![Total Downloads](https://static.pepy.tech/badge/async-httpd-data-collector)](https://pepy.tech/project/async-httpd-data-collector)
715
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/async-httpd-data-collector)](https://pypi.org/project/async-httpd-data-collector/)
716
+ </div>
745
717
 
746
- * `interface.daemon.enableg()`;
747
- * `interface.daemon.disable()`.
718
+ # async-httpd-data-collector
748
719
 
749
- Those methods control the thread within which fetching process is contained.
720
+ > **Note:** This is an older university project and is not actively maintained.
721
+ > It works as-is, but is not extensively tested.
750
722
 
751
- You can query data from the database using methods with `query_` prefix. For now there are three:
723
+ A Python library that acts as an asynchronous gateway between IoT sensor
724
+ devices and InfluxDB. It fetches JSON readings from a device (like a
725
+ NodeMCU/ESP8266) over HTTP, parses them, and stores them as time-series data.
726
+ You can then query the data back as pandas DataFrames.
752
727
 
753
- * `interface.query_latest()`, which queries the lastest measurement;
754
- * `interface.query_historical()`, which queries data from a given time range or relative time (eg. -3h);
755
- * `interface.query()`, which can takes user's given query as an argument.
728
+ Started as a university project to get hands-on with async Python,
729
+ `asyncio`, `aiohttp`, and time-series databases. The hardware side runs on
730
+ an [arduino-air-state-server](https://github.com/straightchlorine/arduino-air-state-server) -
731
+ a NodeMCU board with air quality sensors (MQ135, BMP180, DHT22, DS18B20)
732
+ that exposes readings at an HTTP endpoint.
756
733
 
757
- Some examples will be presented below:
734
+ ## Installation
758
735
 
759
- # 1.1 Connecting to the database
736
+ ```bash
737
+ pip install async-httpd-data-collector
738
+ ```
760
739
 
740
+ ## Quick Start
761
741
 
762
742
  ```python
763
- import json
764
- from ahttpdc.reads.interface import DatabaseInterface
765
-
766
- # load the secrets
767
- with open('../../../secrets/secrets.json', 'r') as f:
768
- secrets = json.load(f)
743
+ from ahttpdc.read.database_interface import DatabaseInterface
769
744
 
770
- # define sensors
745
+ # define which sensors and parameters to track
771
746
  sensors = {
772
- 'bmp180': ['altitude', 'pressure', 'seaLevelPressure'],
747
+ 'bmp180': ['altitude', 'pressure', 'seaLevelPressure', 'temperature'],
773
748
  'mq135': ['aceton', 'alcohol', 'co', 'co2', 'nh4', 'toulen'],
774
749
  'ds18b20': ['temperature'],
775
- 'dht22': ['humidity'],
750
+ 'dht22': ['humidity', 'temperature'],
776
751
  }
777
752
 
778
- # define the interface to the database
753
+ # connect to InfluxDB and the device
779
754
  interface = DatabaseInterface(
780
- secrets['host'],
781
- secrets['port'],
782
- secrets['token'],
783
- secrets['organization'],
784
- secrets['bucket'],
785
755
  sensors,
786
- secrets['dev_ip'],
787
- 80,
788
- secrets['handle'],
756
+ db_host='localhost',
757
+ db_port=8086,
758
+ db_token='your-influxdb-token',
759
+ db_org='your-org',
760
+ db_bucket='your-bucket',
761
+ srv_ip='192.168.1.100', # device IP
762
+ srv_port=80,
763
+ handle='circumstances', # HTTP endpoint on the device
789
764
  )
790
- ```
791
765
 
792
- ### 1.2 Extracting the dataframe from the database
766
+ # start the background daemon - fetches and stores data continuously
767
+ interface.daemon.enable()
793
768
 
769
+ # query the last 30 days of data as a DataFrame
770
+ df = interface.query_historical('-30d')
771
+ print(df.head())
794
772
 
795
- ```python
796
- import pandas as pd
797
- import asyncio
798
- from datetime import datetime, timedelta
799
- from pathlib import Path
800
-
801
- # if there is readings.csv file, load it
802
- # if not - create it
803
- readings_path = Path('../data/readings.csv')
804
- if readings_path.is_file():
805
- sensor = pd.read_csv(readings_path)
806
- else:
807
- sensor = await interface.query_historical('-30d')
808
- sensor.to_csv(readings_path)
773
+ # stop the daemon when done
774
+ interface.daemon.disable()
809
775
  ```
810
776
 
777
+ ## How It Works
811
778
 
812
- ```python
813
- sensor
779
+ ```
780
+ NodeMCU device async-httpd-data-collector InfluxDB
781
+ (sensors) (storage)
782
+ | |
783
+ |--- HTTP GET /circumstances --> AsyncFetcher |
784
+ | | |
785
+ | JSONInfluxParser |
786
+ | | |
787
+ | AsyncCollector --------> |
788
+ | |
789
+ | AsyncQuery <-------- |
790
+ | | |
791
+ | DataParser |
792
+ | | |
793
+ | pandas DataFrame |
814
794
  ```
815
795
 
796
+ The `DatabaseInterface` is the main entry point. It manages two things:
816
797
 
798
+ - **DataDaemon** - a background process (via `multiprocessing`) that
799
+ periodically fetches sensor data from the device and stores it in InfluxDB.
800
+ - **AsyncQuery** - queries InfluxDB and returns results as pandas DataFrames
801
+ with local timezone-adjusted timestamps.
817
802
 
803
+ ### Querying
818
804
 
819
- <div>
820
- <table border="1" class="dataframe">
821
- <thead>
822
- <tr style="text-align: right;">
823
- <th></th>
824
- <th>time</th>
825
- <th>aceton</th>
826
- <th>alcohol</th>
827
- <th>altitude</th>
828
- <th>co</th>
829
- <th>co2</th>
830
- <th>humidity</th>
831
- <th>nh4</th>
832
- <th>pressure</th>
833
- <th>seaLevelPressure</th>
834
- <th>temperature</th>
835
- <th>toulen</th>
836
- </tr>
837
- </thead>
838
- <tbody>
839
- <tr>
840
- <th>0</th>
841
- <td>2024-05-16 17:43:59.196399+00:00</td>
842
- <td>0.41</td>
843
- <td>1.17</td>
844
- <td>149.92</td>
845
- <td>3.38</td>
846
- <td>402.54</td>
847
- <td>37.4</td>
848
- <td>3.93</td>
849
- <td>999.35</td>
850
- <td>1017.31</td>
851
- <td>24.40</td>
852
- <td>0.48</td>
853
- </tr>
854
- <tr>
855
- <th>1</th>
856
- <td>2024-05-16 17:44:01.768738+00:00</td>
857
- <td>0.47</td>
858
- <td>1.32</td>
859
- <td>149.76</td>
860
- <td>3.94</td>
861
- <td>402.84</td>
862
- <td>30.5</td>
863
- <td>4.33</td>
864
- <td>997.61</td>
865
- <td>1015.56</td>
866
- <td>24.03</td>
867
- <td>0.55</td>
868
- </tr>
869
- <tr>
870
- <th>2</th>
871
- <td>2024-05-16 17:44:03.255309+00:00</td>
872
- <td>0.96</td>
873
- <td>2.62</td>
874
- <td>149.54</td>
875
- <td>9.16</td>
876
- <td>405.25</td>
877
- <td>49.1</td>
878
- <td>7.35</td>
879
- <td>999.14</td>
880
- <td>1017.08</td>
881
- <td>23.16</td>
882
- <td>1.15</td>
883
- </tr>
884
- <tr>
885
- <th>3</th>
886
- <td>2024-05-16 17:44:04.618203+00:00</td>
887
- <td>0.30</td>
888
- <td>0.86</td>
889
- <td>149.38</td>
890
- <td>2.32</td>
891
- <td>401.94</td>
892
- <td>32.9</td>
893
- <td>3.10</td>
894
- <td>999.09</td>
895
- <td>1017.02</td>
896
- <td>23.05</td>
897
- <td>0.35</td>
898
- </tr>
899
- <tr>
900
- <th>4</th>
901
- <td>2024-05-16 17:44:05.954714+00:00</td>
902
- <td>1.31</td>
903
- <td>3.50</td>
904
- <td>149.37</td>
905
- <td>13.13</td>
906
- <td>406.82</td>
907
- <td>48.8</td>
908
- <td>9.21</td>
909
- <td>998.04</td>
910
- <td>1015.93</td>
911
- <td>23.92</td>
912
- <td>1.57</td>
913
- </tr>
914
- <tr>
915
- <th>...</th>
916
- <td>...</td>
917
- <td>...</td>
918
- <td>...</td>
919
- <td>...</td>
920
- <td>...</td>
921
- <td>...</td>
922
- <td>...</td>
923
- <td>...</td>
924
- <td>...</td>
925
- <td>...</td>
926
- <td>...</td>
927
- <td>...</td>
928
- </tr>
929
- <tr>
930
- <th>284122</th>
931
- <td>2024-05-21 14:42:57.894312+00:00</td>
932
- <td>1.35</td>
933
- <td>3.62</td>
934
- <td>150.08</td>
935
- <td>13.68</td>
936
- <td>407.03</td>
937
- <td>47.6</td>
938
- <td>9.46</td>
939
- <td>998.85</td>
940
- <td>1016.81</td>
941
- <td>24.35</td>
942
- <td>1.63</td>
943
- </tr>
944
- <tr>
945
- <th>284123</th>
946
- <td>2024-05-21 14:42:59.277937+00:00</td>
947
- <td>1.08</td>
948
- <td>2.92</td>
949
- <td>149.87</td>
950
- <td>10.48</td>
951
- <td>405.79</td>
952
- <td>49.3</td>
953
- <td>8.00</td>
954
- <td>998.58</td>
955
- <td>1016.53</td>
956
- <td>23.41</td>
957
- <td>1.29</td>
958
- </tr>
959
- <tr>
960
- <th>284124</th>
961
- <td>2024-05-21 14:43:00.594968+00:00</td>
962
- <td>0.38</td>
963
- <td>1.09</td>
964
- <td>149.97</td>
965
- <td>3.09</td>
966
- <td>402.38</td>
967
- <td>33.8</td>
968
- <td>3.71</td>
969
- <td>999.59</td>
970
- <td>1017.54</td>
971
- <td>24.88</td>
972
- <td>0.44</td>
973
- </tr>
974
- <tr>
975
- <th>284125</th>
976
- <td>2024-05-21 14:43:01.918239+00:00</td>
977
- <td>1.41</td>
978
- <td>3.77</td>
979
- <td>150.13</td>
980
- <td>14.38</td>
981
- <td>407.29</td>
982
- <td>44.4</td>
983
- <td>9.76</td>
984
- <td>998.51</td>
985
- <td>1016.48</td>
986
- <td>23.54</td>
987
- <td>1.70</td>
988
- </tr>
989
- <tr>
990
- <th>284126</th>
991
- <td>2024-05-21 14:43:03.248095+00:00</td>
992
- <td>1.24</td>
993
- <td>3.32</td>
994
- <td>150.50</td>
995
- <td>12.29</td>
996
- <td>406.50</td>
997
- <td>48.8</td>
998
- <td>8.84</td>
999
- <td>998.85</td>
1000
- <td>1016.85</td>
1001
- <td>22.44</td>
1002
- <td>1.49</td>
1003
- </tr>
1004
- </tbody>
1005
- </table>
1006
- <p>284127 rows × 12 columns</p>
1007
- </div>
805
+ ```python
806
+ # latest reading
807
+ df = interface.query_latest()
808
+
809
+ # last 3 hours
810
+ df = interface.query_historical('-3h')
1008
811
 
812
+ # specific time range
813
+ df = interface.query_historical('2024-05-16T00:00:00Z', '2024-05-21T00:00:00Z')
814
+
815
+ # custom Flux query
816
+ df = interface.query_custom_sync('from(bucket:"my-bucket") |> range(start: -1d) |> last()')
817
+ ```
1009
818
 
819
+ ## Related Projects
1010
820
 
821
+ - [arduino-air-state-server](https://github.com/straightchlorine/arduino-air-state-server) -
822
+ the NodeMCU firmware that collects sensor readings and serves them over HTTP
823
+ - [air-quality-data-analysis](https://github.com/straightchlorine/air-quality-data-analysis) -
824
+ Jupyter notebooks with data analysis (heatmaps, correlations, anomaly detection)
825
+ and SARIMAX time-series forecasting on the collected data