├── .github
├── dependabot.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── publish-to-pypi.yml
│ ├── publish-to-test-pypi.yml
│ ├── pythonpackage.yml
│ └── release_gh.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── LICENSE.txt
├── MANIFEST.in
├── Pipfile
├── Pipfile.lock
├── README.md
├── fixtures
├── camera_data_empty.json
├── camera_data_events_until.json
├── camera_home_data.json
├── camera_home_data_disconnected.json
├── camera_home_data_no_homes.json
├── camera_image_sample.jpg
├── camera_ping.json
├── camera_set_state_error.json
├── camera_set_state_error_already_on.json
├── camera_set_state_error_wrong_parameter.json
├── camera_set_state_ok.json
├── error_scope.json
├── home_coach_no_devices.json
├── home_coach_simple.json
├── home_data_empty.json
├── home_data_no_devices.json
├── home_data_no_homes.json
├── home_data_nohomename.json
├── home_data_simple.json
├── home_status_empty.json
├── home_status_error_and_data.json
├── home_status_error_disconnected.json
├── home_status_error_invalid_id.json
├── home_status_error_invalid_schedule_id.json
├── home_status_error_missing_home_id.json
├── home_status_error_missing_parameters.json
├── home_status_error_mode_is_missing.json
├── home_status_error_mode_not_authorized.json
├── home_status_simple.json
├── invalid_grant.json
├── oauth2_token.json
├── public_data_error_mongo.json
├── public_data_simple.json
├── status_ok.json
├── thermostat_data_simple.json
├── too_many_connections.json
├── weatherstation_data_simple.json
├── weatherstation_data_unreachable_station.json
└── weatherstation_measure.json
├── pyproject.toml
├── setup.cfg
├── setup.py
├── src
├── pyatmo
│ ├── __init__.py
│ ├── __main__.py
│ ├── auth.py
│ ├── camera.py
│ ├── exceptions.py
│ ├── helpers.py
│ ├── home_coach.py
│ ├── public_data.py
│ ├── thermostat.py
│ └── weather_station.py
└── version.py
├── tests
├── __init__.py
├── conftest.py
├── test_pyatmo.py
├── test_pyatmo_camera.py
├── test_pyatmo_homecoach.py
├── test_pyatmo_publicdata.py
├── test_pyatmo_thermostat.py
└── test_pyatmo_weatherstation.py
├── tox.ini
└── usage.md
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | # Check for updates to GitHub Actions every weekday
12 | interval: "daily"
13 |
14 | - package-ecosystem: "pip" # See documentation for possible values
15 | directory: "/" # Location of package manifests
16 | schedule:
17 | interval: "weekly"
18 | # Check for pip updates on Sundays
19 | day: "sunday"
20 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | # ******** NOTE ********
12 |
13 | name: "CodeQL"
14 |
15 | on:
16 | push:
17 | branches: [ development, master ]
18 | pull_request:
19 | # The branches below must be a subset of the branches above
20 | branches: [ development ]
21 | schedule:
22 | - cron: '21 22 * * 0'
23 |
24 | jobs:
25 | analyze:
26 | name: Analyze
27 | runs-on: ubuntu-latest
28 |
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | language: [ 'python' ]
33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
34 | # Learn more...
35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
36 |
37 | steps:
38 | - name: Checkout repository
39 | uses: actions/checkout@v2
40 |
41 | # Initializes the CodeQL tools for scanning.
42 | - name: Initialize CodeQL
43 | uses: github/codeql-action/init@v1
44 | with:
45 | languages: ${{ matrix.language }}
46 | # If you wish to specify custom queries, you can do so here or in a config file.
47 | # By default, queries listed here will override any specified in a config file.
48 | # Prefix the list here with "+" to use these queries and those in the config file.
49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
50 |
51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
52 | # If this step fails, then you should remove it and run the build manually (see below)
53 | - name: Autobuild
54 | uses: github/codeql-action/autobuild@v1
55 |
56 | # ℹ️ Command-line programs to run using the OS shell.
57 | # 📚 https://git.io/JvXDl
58 |
59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
60 | # and modify them (or add more) to build your code if your project
61 | # uses a compiled language
62 |
63 | #- run: |
64 | # make bootstrap
65 | # make release
66 |
67 | - name: Perform CodeQL Analysis
68 | uses: github/codeql-action/analyze@v1
69 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish 📦 to PyPI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - "**/version.py"
9 |
10 | jobs:
11 | build-n-publish:
12 | name: Build and publish 📦 to PyPI
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@master
16 | - name: Set up Python 3.7
17 | uses: actions/setup-python@v2.1.4
18 | with:
19 | python-version: 3.7
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install setuptools wheel
24 | - name: Build a binary wheel and a source tarball
25 | run: >-
26 | python setup.py sdist bdist_wheel
27 | - name: Publish 📦 to PyPI
28 | uses: pypa/gh-action-pypi-publish@master
29 | with:
30 | password: ${{ secrets.pypi_prod_token }}
31 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-test-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish 📦 to TestPyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build-n-publish:
10 | name: Build and publish 📦 to TestPyPI
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | ref: development
16 | - name: Set up Python 3.7
17 | uses: actions/setup-python@v2.1.4
18 | with:
19 | python-version: 3.7
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install setuptools wheel
24 | - name: Build a binary wheel and a source tarball
25 | run: >-
26 | python setup.py sdist bdist_wheel
27 | - name: Publish 📦 to Test PyPI
28 | uses: pypa/gh-action-pypi-publish@master
29 | with:
30 | password: ${{ secrets.PYPI_TEST_TOKEN }}
31 | repository_url: https://test.pypi.org/legacy/
32 |
--------------------------------------------------------------------------------
/.github/workflows/pythonpackage.yml:
--------------------------------------------------------------------------------
1 | name: Python package
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - development
8 | pull_request:
9 | branches:
10 | - master
11 | - development
12 |
13 | jobs:
14 | black:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | max-parallel: 1
18 | matrix:
19 | python-version: [3.7]
20 |
21 | steps:
22 | - uses: actions/checkout@v2.3.3
23 | - name: Set up Python ${{ matrix.python-version }}
24 | uses: actions/setup-python@v2.1.4
25 | with:
26 | python-version: ${{ matrix.python-version }}
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install black
31 | - name: Check with black
32 | run: |
33 | black --check --diff src/pyatmo/ tests/ setup.py
34 |
35 | linter:
36 | runs-on: ubuntu-latest
37 | strategy:
38 | max-parallel: 4
39 | matrix:
40 | python-version: [ 3.8 ]
41 |
42 | steps:
43 | - uses: actions/checkout@v2.3.3
44 | - name: Set up Python ${{ matrix.python-version }}
45 | uses: actions/setup-python@v2.1.4
46 | with:
47 | python-version: ${{ matrix.python-version }}
48 | - name: Install dependencies
49 | run: |
50 | python -m pip install --upgrade pip
51 | pip install flake8
52 | - name: Lint with flake8
53 | run: |
54 | # stop the build if there are Python syntax errors or undefined names
55 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
56 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
57 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
58 |
59 | build:
60 |
61 | runs-on: ubuntu-latest
62 | strategy:
63 | max-parallel: 4
64 | matrix:
65 | python-version: [3.7, 3.8, 3.9]
66 |
67 | steps:
68 | - uses: actions/checkout@v2.3.3
69 | - name: Set up Python ${{ matrix.python-version }}
70 | uses: actions/setup-python@v2.1.4
71 | with:
72 | python-version: ${{ matrix.python-version }}
73 | - name: Install dependencies
74 | run: |
75 | python -m pip install --upgrade pip
76 | pip install -e .
77 | - name: Run tests with tox
78 | run: |
79 | pip install tox tox-gh-actions
80 | tox
81 |
--------------------------------------------------------------------------------
/.github/workflows/release_gh.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Create Github Release
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the development branch
7 | on:
8 | push:
9 | # Sequence of patterns matched against refs/tags
10 | tags:
11 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
12 |
13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
14 | jobs:
15 | # This workflow contains a single job called "build"
16 | build:
17 | # The type of runner that the job will run on
18 | runs-on: ubuntu-latest
19 |
20 | # Steps represent a sequence of tasks that will be executed as part of the job
21 | steps:
22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
23 | - uses: actions/checkout@v2
24 |
25 | # Runs a single command using the runners shell
26 | - name: Create a Release
27 | uses: actions/create-release@v1.1.4
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | with:
31 | # The name of the tag. This should come from the webhook payload, `github.GITHUB_REF` when a user pushes a new tag
32 | tag_name: ${{ github.ref }}
33 | # The name of the release. For example, `Release v1.0.1`
34 | release_name: Release ${{ github.ref }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | build/
3 |
4 | dist/
5 |
6 | pyatmo.egg-info/
7 |
8 | *.pyc
9 | .DS_Store
10 |
11 | archive/
12 | access.token
13 | cov.xml
14 |
15 | venv/
16 | .venv
17 |
18 | # Created by https://www.gitignore.io/api/python,pycharm
19 | # Edit at https://www.gitignore.io/?templates=python,pycharm
20 |
21 | ### PyCharm ###
22 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
23 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
24 |
25 | # User-specific stuff
26 | .idea/**/workspace.xml
27 | .idea/**/tasks.xml
28 | .idea/**/usage.statistics.xml
29 | .idea/**/dictionaries
30 | .idea/**/shelf
31 |
32 | # Generated files
33 | .idea/**/contentModel.xml
34 |
35 | # Sensitive or high-churn files
36 | .idea/**/dataSources/
37 | .idea/**/dataSources.ids
38 | .idea/**/dataSources.local.xml
39 | .idea/**/sqlDataSources.xml
40 | .idea/**/dynamic.xml
41 | .idea/**/uiDesigner.xml
42 | .idea/**/dbnavigator.xml
43 |
44 | # Gradle
45 | .idea/**/gradle.xml
46 | .idea/**/libraries
47 |
48 | # Gradle and Maven with auto-import
49 | # When using Gradle or Maven with auto-import, you should exclude module files,
50 | # since they will be recreated, and may cause churn. Uncomment if using
51 | # auto-import.
52 | # .idea/modules.xml
53 | # .idea/*.iml
54 | # .idea/modules
55 | # *.iml
56 | # *.ipr
57 |
58 | # CMake
59 | cmake-build-*/
60 |
61 | # Mongo Explorer plugin
62 | .idea/**/mongoSettings.xml
63 |
64 | # File-based project format
65 | *.iws
66 |
67 | # IntelliJ
68 | out/
69 |
70 | # mpeltonen/sbt-idea plugin
71 | .idea_modules/
72 |
73 | # JIRA plugin
74 | atlassian-ide-plugin.xml
75 |
76 | # Cursive Clojure plugin
77 | .idea/replstate.xml
78 |
79 | # Crashlytics plugin (for Android Studio and IntelliJ)
80 | com_crashlytics_export_strings.xml
81 | crashlytics.properties
82 | crashlytics-build.properties
83 | fabric.properties
84 |
85 | # Editor-based Rest Client
86 | .idea/httpRequests
87 |
88 | # Android studio 3.1+ serialized cache file
89 | .idea/caches/build_file_checksums.ser
90 |
91 | ### PyCharm Patch ###
92 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
93 |
94 | # *.iml
95 | # modules.xml
96 | # .idea/misc.xml
97 | # *.ipr
98 |
99 | # Sonarlint plugin
100 | .idea/**/sonarlint/
101 |
102 | # SonarQube Plugin
103 | .idea/**/sonarIssues.xml
104 |
105 | # Markdown Navigator plugin
106 | .idea/**/markdown-navigator.xml
107 | .idea/**/markdown-navigator/
108 |
109 | ### Python ###
110 | # Byte-compiled / optimized / DLL files
111 | __pycache__/
112 | *.py[cod]
113 | *$py.class
114 |
115 | # C extensions
116 | *.so
117 |
118 | # Distribution / packaging
119 | .Python
120 | build/
121 | develop-eggs/
122 | dist/
123 | downloads/
124 | eggs/
125 | .eggs/
126 | lib/
127 | lib64/
128 | parts/
129 | sdist/
130 | var/
131 | wheels/
132 | pip-wheel-metadata/
133 | share/python-wheels/
134 | *.egg-info/
135 | .installed.cfg
136 | *.egg
137 | MANIFEST
138 |
139 | # PyInstaller
140 | # Usually these files are written by a python script from a template
141 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
142 | *.manifest
143 | *.spec
144 |
145 | # Installer logs
146 | pip-log.txt
147 | pip-delete-this-directory.txt
148 |
149 | # Unit test / coverage reports
150 | htmlcov/
151 | .tox/
152 | .nox/
153 | .coverage
154 | .coverage.*
155 | .cache
156 | nosetests.xml
157 | coverage.xml
158 | *.cover
159 | .hypothesis/
160 | .pytest_cache/
161 |
162 | # Translations
163 | *.mo
164 | *.pot
165 |
166 | # Scrapy stuff:
167 | .scrapy
168 |
169 | # Sphinx documentation
170 | docs/_build/
171 |
172 | # PyBuilder
173 | target/
174 |
175 | # pyenv
176 | .python-version
177 |
178 | # pipenv
179 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
180 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
181 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
182 | # install all needed dependencies.
183 | #Pipfile.lock
184 |
185 | # celery beat schedule file
186 | celerybeat-schedule
187 |
188 | # SageMath parsed files
189 | *.sage.py
190 |
191 | # Spyder project settings
192 | .spyderproject
193 | .spyproject
194 |
195 | # Rope project settings
196 | .ropeproject
197 |
198 | # Mr Developer
199 | .mr.developer.cfg
200 | .project
201 | .pydevproject
202 |
203 | # mkdocs documentation
204 | /site
205 |
206 | # mypy
207 | .mypy_cache/
208 | .dmypy.json
209 | dmypy.json
210 |
211 | # Pyre type checker
212 | .pyre/
213 |
214 | # End of https://www.gitignore.io/api/python,pycharm
215 |
216 | # Created by https://www.gitignore.io/api/code
217 | # Edit at https://www.gitignore.io/?templates=code
218 |
219 | ### Code ###
220 | .vscode/*
221 |
222 | # End of https://www.gitignore.io/api/code
223 |
224 | # Created by https://www.gitignore.io/api/jetbrains
225 | # Edit at https://www.gitignore.io/?templates=jetbrains
226 |
227 | ### JetBrains ###
228 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
229 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
230 |
231 | # User-specific stuff
232 | .idea/**/workspace.xml
233 | .idea/**/tasks.xml
234 | .idea/**/usage.statistics.xml
235 | .idea/**/dictionaries
236 | .idea/**/shelf
237 |
238 | # Generated files
239 | .idea/**/contentModel.xml
240 |
241 | # Sensitive or high-churn files
242 | .idea/**/dataSources/
243 | .idea/**/dataSources.ids
244 | .idea/**/dataSources.local.xml
245 | .idea/**/sqlDataSources.xml
246 | .idea/**/dynamic.xml
247 | .idea/**/uiDesigner.xml
248 | .idea/**/dbnavigator.xml
249 |
250 | # Gradle
251 | .idea/**/gradle.xml
252 | .idea/**/libraries
253 |
254 | # Gradle and Maven with auto-import
255 | # When using Gradle or Maven with auto-import, you should exclude module files,
256 | # since they will be recreated, and may cause churn. Uncomment if using
257 | # auto-import.
258 | # .idea/modules.xml
259 | # .idea/*.iml
260 | # .idea/modules
261 | # *.iml
262 | # *.ipr
263 |
264 | # CMake
265 | cmake-build-*/
266 |
267 | # Mongo Explorer plugin
268 | .idea/**/mongoSettings.xml
269 |
270 | # File-based project format
271 | *.iws
272 |
273 | # IntelliJ
274 | out/
275 |
276 | # mpeltonen/sbt-idea plugin
277 | .idea_modules/
278 |
279 | # JIRA plugin
280 | atlassian-ide-plugin.xml
281 |
282 | # Cursive Clojure plugin
283 | .idea/replstate.xml
284 |
285 | # Crashlytics plugin (for Android Studio and IntelliJ)
286 | com_crashlytics_export_strings.xml
287 | crashlytics.properties
288 | crashlytics-build.properties
289 | fabric.properties
290 |
291 | # Editor-based Rest Client
292 | .idea/httpRequests
293 |
294 | # Android studio 3.1+ serialized cache file
295 | .idea/caches/build_file_checksums.ser
296 |
297 | ### JetBrains Patch ###
298 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
299 |
300 | # *.iml
301 | # modules.xml
302 | # .idea/misc.xml
303 | # *.ipr
304 |
305 | # Sonarlint plugin
306 | .idea/**/sonarlint/
307 |
308 | # SonarQube Plugin
309 | .idea/**/sonarIssues.xml
310 |
311 | # Markdown Navigator plugin
312 | .idea/**/markdown-navigator.xml
313 | .idea/**/markdown-navigator/
314 |
315 | # End of https://www.gitignore.io/api/jetbrains
316 | .idea
317 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # Note: don't use this config for your own repositories. Instead, see
2 | # "Version control integration" in README.md.
3 | default_stages: [commit, push]
4 | exclude: ^(fixtures/)
5 | repos:
6 | - repo: https://github.com/asottile/seed-isort-config
7 | rev: v2.2.0
8 | hooks:
9 | - id: seed-isort-config
10 | args: [--application-directories,./src]
11 |
12 | - repo: https://github.com/asottile/pyupgrade
13 | rev: v2.7.4
14 | hooks:
15 | - id: pyupgrade
16 | args: [--py37-plus]
17 | exclude: 'external_src/int-tools'
18 |
19 | - repo: https://github.com/asottile/add-trailing-comma
20 | rev: v2.0.1
21 | hooks:
22 | - id: add-trailing-comma
23 | args: [ --py36-plus ]
24 | exclude: 'external_src/int-tools'
25 |
26 | - repo: https://github.com/asottile/yesqa
27 | rev: v1.2.2
28 | hooks:
29 | - id: yesqa
30 | - repo: local
31 | hooks:
32 | - id: isort
33 | name: isort
34 | language: system
35 | entry: pipenv run isort
36 | types: [python]
37 | exclude: tests/
38 |
39 | - id: black
40 | name: black
41 | language: system
42 | entry: pipenv run black
43 | types: [python]
44 |
45 | - id: pylint
46 | name: pylint
47 | language: system
48 | entry: pipenv run pylint
49 | types: [python]
50 |
51 | - id: mypy
52 | name: mypy
53 | language: system
54 | entry: pipenv run mypy
55 | types: [python]
56 | exclude: tests/
57 |
58 | - repo: https://github.com/pre-commit/pre-commit-hooks
59 | rev: v3.3.0 # Use the ref you want to point at
60 | hooks:
61 | - id: check-ast
62 | - id: no-commit-to-branch
63 | args: [--branch, master, --branch, devel]
64 | - id: forbid-new-submodules
65 | - id: check-merge-conflict
66 | - id: detect-private-key
67 | - id: end-of-file-fixer
68 | - id: mixed-line-ending
69 | args: [--fix=lf]
70 | - id: trailing-whitespace
71 | - id: debug-statements
72 | - id: check-toml
73 |
74 | - repo: https://gitlab.com/pycqa/flake8
75 | rev: 3.8.4 # pick a git hash / tag to point to
76 | hooks:
77 | - id: flake8
78 | exclude: (otp)
79 | additional_dependencies: [flake8-typing-imports==1.10.0]
80 |
81 | - repo: https://github.com/asottile/setup-cfg-fmt
82 | rev: v1.15.1
83 | hooks:
84 | - id: setup-cfg-fmt
85 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 | # Reasons disabled:
3 | # duplicate-code - unavoidable
4 | # cyclic-import - doesn't test if both import on load
5 | # unused-argument - generic callbacks and setup methods create a lot of warnings
6 | # global-statement - used for the on-demand requirement installation
7 | # too-many-* - are not enforced for the sake of readability
8 | # too-few-* - same as too-many-*
9 | # abstract-method - with intro of async there are always methods missing
10 | # not-an-iterable - https://github.com/PyCQA/pylint/issues/2311
11 | disable=
12 | abstract-method,
13 | cyclic-import,
14 | duplicate-code,
15 | global-statement,
16 | inconsistent-return-statements,
17 | missing-docstring,
18 | too-few-public-methods,
19 | too-many-arguments,
20 | too-many-branches,
21 | too-many-instance-attributes,
22 | too-many-lines,
23 | too-many-locals,
24 | too-many-public-methods,
25 | too-many-return-statements,
26 | too-many-statements,
27 | abstract-method,
28 | not-an-iterable,
29 | format,
30 |
31 | [REPORTS]
32 | reports=no
33 |
34 | [TYPECHECK]
35 | # For attrs
36 | ignored-classes=_CountingAttr
37 | generated-members=botocore.errorfactory
38 |
39 | [FORMAT]
40 | expected-line-ending-format=LF
41 |
42 | [EXCEPTIONS]
43 | overgeneral-exceptions=Exception
44 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Hugo DUPRAS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE.txt
3 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | black = "*"
8 | bleach = "~=3.1,>3.1.4"
9 | docutils = "*"
10 | flake8 = "*"
11 | freezegun = "*"
12 | isort = "*"
13 | mypy = "*"
14 | pre-commit = "*"
15 | pylint = "*"
16 | pytest = "*"
17 | pytest-cov = "*"
18 | pytest-mock = "*"
19 | requests-mock = "*"
20 | tox = "*"
21 | twine = "*"
22 |
23 | [packages]
24 | requests = "*"
25 | requests-oauthlib = "*"
26 | pyatmo = {editable = true,path = "."}
27 |
28 | [requires]
29 | python_version = "3.7"
30 |
31 | [pipenv]
32 | allow_prereleases = true
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: The repo has been moved to https://github.com/jabesq/pyatmo :warning:
2 |
3 | netatmo-api-python
4 | ==================
5 |
6 | [](https://github.com/ambv/black)
7 | [](https://github.com/jabesq/netatmo-api-python/actions?workflow=Python+package)
8 | [](https://pypi.python.org/pypi/pyatmo)
9 | [](https://github.com/jabesq/netatmo-api-python/blob/master/LICENSE.txt)
10 |
11 | Simple API to access Netatmo devices and data like weather station or camera data from Python 3.
12 | For more detailed information see [dev.netatmo.com](http://dev.netatmo.com)
13 |
14 | This project has no relation with the Netatmo company.
15 |
16 | Install
17 | -------
18 |
19 | To install pyatmo simply run:
20 |
21 | pip install pyatmo
22 |
23 | Depending on your permissions you might be required to use sudo.
24 | Once installed you can simple add `pyatmo` to your Python 3 scripts by including:
25 |
26 | import pyatmo
27 |
28 | Note
29 | ----
30 |
31 | The module requires a valid user account and a registered application. See [usage.md](./usage.md) for further information.
32 | Be aware that the module may stop working if Netatmo decides to change their API.
33 |
34 | Development
35 | -----------
36 |
37 | Clone the repo and install dependencies:
38 |
39 | git clone
40 | cd netatmo-api-python
41 | pipenv install --dev
42 |
43 | To add the pre-commit hook to your environment run:
44 |
45 | pip install pre-commit
46 | pre-commit install
47 |
48 | Testing
49 | -------
50 |
51 | To run the full suite simply run the following command from within the virtual environment:
52 |
53 | pytest
54 |
55 | or
56 |
57 | python -m pytest tests/
58 |
59 | To generate code coverage xml (e.g. for use in VSCode) run
60 |
61 | python -m pytest --cov-report xml:cov.xml --cov smart_home --cov-append tests/
62 |
63 | Another way to run the tests is by using `tox`. This runs the tests against the installed package and multiple versions of python.
64 |
65 | tox
66 |
67 | or by specifying a python version
68 |
69 | tox -e py38
70 |
--------------------------------------------------------------------------------
/fixtures/camera_data_empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_exec": 0.03621506690979,
4 | "time_server": 1560626960
5 | }
--------------------------------------------------------------------------------
/fixtures/camera_data_events_until.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "events_list": [
4 | {
5 | "id": "a1b2c3d4e5f6abcdef123461",
6 | "type": "person",
7 | "time": 1560706232,
8 | "camera_id": "12:34:56:00:f1:62",
9 | "device_id": "12:34:56:00:f1:62",
10 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
11 | "video_status": "deleted",
12 | "is_arrival": true,
13 | "message": "John Doe gesehen"
14 | },
15 | {
16 | "id": "a1b2c3d4e5f6abcdef123462",
17 | "type": "person_away",
18 | "time": 1560706237,
19 | "camera_id": "12:34:56:00:f1:62",
20 | "device_id": "12:34:56:00:f1:62",
21 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
22 | "message": "John Doe hat das Haus verlassen",
23 | "sub_message": "John Doe gilt als „Abwesend“, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat."
24 | },
25 | {
26 | "id": "a1b2c3d4e5f6abcdef123463",
27 | "type": "person",
28 | "time": 1560706241,
29 | "camera_id": "12:34:56:00:f1:62",
30 | "device_id": "12:34:56:00:f1:62",
31 | "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2",
32 | "snapshot": {
33 | "id": "19b13efa945ec892c6da2a8c",
34 | "version": 1,
35 | "key": "1704853cfc9571bd10618591dc9035e5bc0fa3203f44739c49a5b26d2f7ad67f",
36 | "url": "https://netatmocameraimage.blob.core.windows.net/production/5ecfa94c6da5e5bc0fa3203f3cfdc903489219b13e2a8c548547b26d2f7ad6717039c49ac9571bd10618591f"
37 | },
38 | "video_id": "f914-aa7da416643-4744-82f9-4e7d4440b",
39 | "video_status": "available",
40 | "is_arrival": false,
41 | "message": "Jane Doe gesehen"
42 | },
43 | {
44 | "id": "a1b2c3d4e5f6abcdef123464",
45 | "type": "wifi_status",
46 | "time": 1560706271,
47 | "camera_id": "12:34:56:00:8b:a2",
48 | "device_id": "12:34:56:00:8b:a2",
49 | "sub_type": 1,
50 | "message": "Hall:WLAN-Verbindung erfolgreich hergestellt"
51 | },
52 | {
53 | "id": "a1b2c3d4e5f6abcdef123465",
54 | "type": "outdoor",
55 | "time": 1560706283,
56 | "camera_id": "12:34:56:00:a5:a4",
57 | "device_id": "12:34:56:00:a5:a4",
58 | "video_id": "string",
59 | "video_status": "available",
60 | "event_list": [
61 | {
62 | "type": "string",
63 | "time": 1560706283,
64 | "offset": 0,
65 | "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0001",
66 | "message": "Animal détecté",
67 | "snapshot": {
68 | "id": "5715e16849c75xxxx00000000xxxxx",
69 | "version": 1,
70 | "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
71 | "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa"
72 | },
73 | "vignette": {
74 | "id": "5715e16849c75xxxx00000000xxxxx",
75 | "version": 1,
76 | "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
77 | "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000"
78 | }
79 | },
80 | {
81 | "type": "string",
82 | "time": 1560706283,
83 | "offset": 0,
84 | "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0002",
85 | "message": "Animal détecté",
86 | "snapshot": {
87 | "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg"
88 | },
89 | "vignette": {
90 | "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg"
91 | }
92 | }
93 | ]
94 | }
95 | ]
96 | },
97 | "status": "ok",
98 | "time_exec": 0.03666909215079,
99 | "time_server": 15607062321
100 | }
--------------------------------------------------------------------------------
/fixtures/camera_home_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [
4 | {
5 | "id": "91763b24c43d3e344f424e8b",
6 | "name": "MYHOME",
7 | "persons": [
8 | {
9 | "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
10 | "last_seen": 1557071156,
11 | "out_of_sight": true,
12 | "face": {
13 | "id": "d74fad765b9100ef480720a9",
14 | "version": 1,
15 | "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7",
16 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
17 | },
18 | "pseudo": "John Doe"
19 | },
20 | {
21 | "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
22 | "last_seen": 1560600726,
23 | "out_of_sight": true,
24 | "face": {
25 | "id": "d74fad765b9100ef480720a9",
26 | "version": 3,
27 | "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
28 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
29 | },
30 | "pseudo": "Jane Doe"
31 | },
32 | {
33 | "id": "91827376-7e04-5298-83af-a0cb8372dff3",
34 | "last_seen": 1560626666,
35 | "out_of_sight": false,
36 | "face": {
37 | "id": "d74fad765b9100ef480720a9",
38 | "version": 1,
39 | "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8",
40 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
41 | },
42 | "pseudo": "Richard Doe"
43 | },
44 | {
45 | "id": "91827376-7e04-5298-83af-a0cb8372dff4",
46 | "last_seen": 1560621666,
47 | "out_of_sight": true,
48 | "face": {
49 | "id": "d0ef44fad765b980720710a9",
50 | "version": 1,
51 | "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928",
52 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928"
53 | }
54 | }
55 | ],
56 | "place": {
57 | "city": "Frankfurt",
58 | "country": "DE",
59 | "timezone": "Europe/Berlin"
60 | },
61 | "cameras": [
62 | {
63 | "id": "12:34:56:00:f1:62",
64 | "type": "NACamera",
65 | "status": "on",
66 | "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,",
67 | "is_local": true,
68 | "sd_status": "on",
69 | "alim_status": "on",
70 | "name": "Hall",
71 | "modules": [
72 | {
73 | "id": "12:34:56:00:f2:f1",
74 | "type": "NIS",
75 | "battery_percent": 84,
76 | "rf": 68,
77 | "status": "no_news",
78 | "monitoring": "on",
79 | "alim_source": "battery",
80 | "tamper_detection_enabled": true,
81 | "name": "Welcome's Siren"
82 | }
83 | ],
84 | "use_pin_code": false,
85 | "last_setup": 1544828430
86 | },
87 | {
88 | "id": "12:34:56:00:a5:a4",
89 | "type": "NOC",
90 | "status": "on",
91 | "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,",
92 | "is_local": true,
93 | "sd_status": "on",
94 | "alim_status": "on",
95 | "name": "Garden",
96 | "last_setup": 1563737661,
97 | "light_mode_status": "auto"
98 | }
99 | ],
100 | "smokedetectors": [
101 | {
102 | "id": "12:34:56:00:8b:a2",
103 | "type": "NSD",
104 | "last_setup": 1567261859,
105 | "name": "Hall"
106 | },
107 | {
108 | "id": "12:34:56:00:8b:ac",
109 | "type": "NSD",
110 | "last_setup": 1567262759,
111 | "name": "Kitchen"
112 | }
113 | ],
114 | "events": [
115 | {
116 | "id": "a1b2c3d4e5f6abcdef123456",
117 | "type": "person",
118 | "time": 1560604700,
119 | "camera_id": "12:34:56:00:f1:62",
120 | "device_id": "12:34:56:00:f1:62",
121 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
122 | "video_status": "deleted",
123 | "is_arrival": false,
124 | "message": "John Doe gesehen"
125 | },
126 | {
127 | "id": "a1b2c3d4e5f6abcdef123457",
128 | "type": "person_away",
129 | "time": 1560602400,
130 | "camera_id": "12:34:56:00:f1:62",
131 | "device_id": "12:34:56:00:f1:62",
132 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
133 | "message": "John Doe hat das Haus verlassen",
134 | "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat."
135 | },
136 | {
137 | "id": "a1b2c3d4e5f6abcdef123458",
138 | "type": "person",
139 | "time": 1560601200,
140 | "camera_id": "12:34:56:00:f1:62",
141 | "device_id": "12:34:56:00:f1:62",
142 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
143 | "video_status": "deleted",
144 | "is_arrival": false,
145 | "message": "John Doe gesehen"
146 | },
147 | {
148 | "id": "a1b2c3d4e5f6abcdef123459",
149 | "type": "person",
150 | "time": 1560600100,
151 | "camera_id": "12:34:56:00:f1:62",
152 | "device_id": "12:34:56:00:f1:62",
153 | "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2",
154 | "snapshot": {
155 | "id": "d74fad765b9100ef480720a9",
156 | "version": 1,
157 | "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
158 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
159 | },
160 | "video_id": "12345678-36bc-4b9a-9762-5194e707ed51",
161 | "video_status": "available",
162 | "is_arrival": false,
163 | "message": "Jane Doe gesehen"
164 | },
165 | {
166 | "id": "a1b2c3d4e5f6abcdef12345a",
167 | "type": "person",
168 | "time": 1560603600,
169 | "camera_id": "12:34:56:00:f1:62",
170 | "device_id": "12:34:56:00:f1:62",
171 | "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3",
172 | "snapshot": {
173 | "id": "532dde8d17554c022ab071b8",
174 | "version": 1,
175 | "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
176 | "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
177 | },
178 | "video_id": "12345678-1234-46cb-ad8f-23d893874099",
179 | "video_status": "available",
180 | "is_arrival": false,
181 | "message": "Bewegung erkannt"
182 | },
183 | {
184 | "id": "a1b2c3d4e5f6abcdef12345b",
185 | "type": "movement",
186 | "time": 1560506200,
187 | "camera_id": "12:34:56:00:f1:62",
188 | "device_id": "12:34:56:00:f1:62",
189 | "category": "human",
190 | "snapshot": {
191 | "id": "532dde8d17554c022ab071b9",
192 | "version": 1,
193 | "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
194 | "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
195 | },
196 | "vignette": {
197 | "id": "5dc021b5dea854bd2321707a",
198 | "version": 1,
199 | "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944",
200 | "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944"
201 | },
202 | "video_id": "12345678-1234-46cb-ad8f-23d89387409a",
203 | "video_status": "available",
204 | "message": "Bewegung erkannt"
205 | },
206 | {
207 | "id": "a1b2c3d4e5f6abcdef12345c",
208 | "type": "sound_test",
209 | "time": 1560506210,
210 | "camera_id": "12:34:56:00:8b:a2",
211 | "device_id": "12:34:56:00:8b:a2",
212 | "sub_type": 0,
213 | "message": "Hall: Alarmton erfolgreich getestet"
214 | },
215 | {
216 | "id": "a1b2c3d4e5f6abcdef12345d",
217 | "type": "wifi_status",
218 | "time": 1560506220,
219 | "camera_id": "12:34:56:00:8b:a2",
220 | "device_id": "12:34:56:00:8b:a2",
221 | "sub_type": 1,
222 | "message": "Hall:WLAN-Verbindung erfolgreich hergestellt"
223 | },
224 | {
225 | "id": "a1b2c3d4e5f6abcdef12345e",
226 | "type": "outdoor",
227 | "time": 1560643100,
228 | "camera_id": "12:34:56:00:a5:a4",
229 | "device_id": "12:34:56:00:a5:a4",
230 | "video_id": "string",
231 | "video_status": "available",
232 | "event_list": [
233 | {
234 | "type": "string",
235 | "time": 1560643100,
236 | "offset": 0,
237 | "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000",
238 | "message": "Animal détecté",
239 | "snapshot": {
240 | "id": "5715e16849c75xxxx00000000xxxxx",
241 | "version": 1,
242 | "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
243 | "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa"
244 | },
245 | "vignette": {
246 | "id": "5715e16849c75xxxx00000000xxxxx",
247 | "version": 1,
248 | "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
249 | "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000"
250 | }
251 | },
252 | {
253 | "type": "string",
254 | "time": 1560506222,
255 | "offset": 0,
256 | "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000",
257 | "message": "Animal détecté",
258 | "snapshot": {
259 | "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg"
260 | },
261 | "vignette": {
262 | "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg"
263 | }
264 | }
265 | ]
266 | }
267 | ]
268 | },
269 | {
270 | "id": "91763b24c43d3e344f424e8c",
271 | "persons": [],
272 | "place": {
273 | "city": "Frankfurt",
274 | "country": "DE",
275 | "timezone": "Europe/Berlin"
276 | },
277 | "cameras": [
278 | {
279 | "id": "12:34:56:00:a5:a5",
280 | "type": "NOC",
281 | "status": "on",
282 | "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,",
283 | "is_local": true,
284 | "sd_status": "on",
285 | "alim_status": "on",
286 | "name": "Street",
287 | "last_setup": 1563737561,
288 | "light_mode_status": "auto"
289 | }
290 | ],
291 | "smokedetectors": []
292 | },
293 | {
294 | "id": "91763b24c43d3e344f424e8d",
295 | "persons": [],
296 | "place": {
297 | "city": "Frankfurt",
298 | "country": "DE",
299 | "timezone": "Europe/Berlin"
300 | },
301 | "cameras": [],
302 | "smokedetectors": []
303 | }
304 | ],
305 | "user": {
306 | "reg_locale": "de-DE",
307 | "lang": "de-DE",
308 | "country": "DE",
309 | "mail": "john@doe.com"
310 | },
311 | "global_info": {
312 | "show_tags": true
313 | }
314 | },
315 | "status": "ok",
316 | "time_exec": 0.03621506690979,
317 | "time_server": 1560626960
318 | }
--------------------------------------------------------------------------------
/fixtures/camera_home_data_disconnected.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [
4 | {
5 | "id": "91763b24c43d3e344f424e8b",
6 | "name": "MYHOME",
7 | "persons": [
8 | {
9 | "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
10 | "last_seen": 1557071156,
11 | "out_of_sight": true,
12 | "face": {
13 | "id": "d74fad765b9100ef480720a9",
14 | "version": 1,
15 | "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7",
16 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
17 | },
18 | "pseudo": "John Doe"
19 | },
20 | {
21 | "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
22 | "last_seen": 1560600726,
23 | "out_of_sight": true,
24 | "face": {
25 | "id": "d74fad765b9100ef480720a9",
26 | "version": 3,
27 | "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
28 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
29 | },
30 | "pseudo": "Jane Doe"
31 | },
32 | {
33 | "id": "91827376-7e04-5298-83af-a0cb8372dff3",
34 | "last_seen": 1560626666,
35 | "out_of_sight": false,
36 | "face": {
37 | "id": "d74fad765b9100ef480720a9",
38 | "version": 1,
39 | "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8",
40 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
41 | },
42 | "pseudo": "Richard Doe"
43 | },
44 | {
45 | "id": "91827376-7e04-5298-83af-a0cb8372dff4",
46 | "last_seen": 1560621666,
47 | "out_of_sight": true,
48 | "face": {
49 | "id": "d0ef44fad765b980720710a9",
50 | "version": 1,
51 | "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928",
52 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928"
53 | }
54 | }
55 | ],
56 | "place": {
57 | "city": "Frankfurt",
58 | "country": "DE",
59 | "timezone": "Europe/Berlin"
60 | },
61 | "cameras": [
62 | {
63 | "id": "12:34:56:00:f1:62",
64 | "type": "NACamera",
65 | "status": "disconnected",
66 | "sd_status": "on",
67 | "alim_status": "on",
68 | "name": "Hall",
69 | "use_pin_code": false,
70 | "last_setup": 1544828430
71 | }
72 | ],
73 | "smokedetectors": [],
74 | "events": [
75 | {
76 | "id": "a1b2c3d4e5f6abcdef123456",
77 | "type": "person",
78 | "time": 1560604700,
79 | "camera_id": "12:34:56:00:f1:62",
80 | "device_id": "12:34:56:00:f1:62",
81 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
82 | "video_status": "deleted",
83 | "is_arrival": false,
84 | "message": "John Doe gesehen"
85 | },
86 | {
87 | "id": "a1b2c3d4e5f6abcdef123457",
88 | "type": "person_away",
89 | "time": 1560602400,
90 | "camera_id": "12:34:56:00:f1:62",
91 | "device_id": "12:34:56:00:f1:62",
92 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
93 | "message": "John Doe hat das Haus verlassen",
94 | "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat."
95 | },
96 | {
97 | "id": "a1b2c3d4e5f6abcdef123458",
98 | "type": "person",
99 | "time": 1560601200,
100 | "camera_id": "12:34:56:00:f1:62",
101 | "device_id": "12:34:56:00:f1:62",
102 | "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
103 | "video_status": "deleted",
104 | "is_arrival": false,
105 | "message": "John Doe gesehen"
106 | },
107 | {
108 | "id": "a1b2c3d4e5f6abcdef123459",
109 | "type": "person",
110 | "time": 1560600100,
111 | "camera_id": "12:34:56:00:f1:62",
112 | "device_id": "12:34:56:00:f1:62",
113 | "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2",
114 | "snapshot": {
115 | "id": "d74fad765b9100ef480720a9",
116 | "version": 1,
117 | "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
118 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
119 | },
120 | "video_id": "12345678-36bc-4b9a-9762-5194e707ed51",
121 | "video_status": "available",
122 | "is_arrival": false,
123 | "message": "Jane Doe gesehen"
124 | },
125 | {
126 | "id": "a1b2c3d4e5f6abcdef12345a",
127 | "type": "person",
128 | "time": 1560603600,
129 | "camera_id": "12:34:56:00:f1:62",
130 | "device_id": "12:34:56:00:f1:62",
131 | "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3",
132 | "snapshot": {
133 | "id": "532dde8d17554c022ab071b8",
134 | "version": 1,
135 | "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
136 | "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
137 | },
138 | "video_id": "12345678-1234-46cb-ad8f-23d893874099",
139 | "video_status": "available",
140 | "is_arrival": false,
141 | "message": "Bewegung erkannt"
142 | },
143 | {
144 | "id": "a1b2c3d4e5f6abcdef12345b",
145 | "type": "movement",
146 | "time": 1560506200,
147 | "camera_id": "12:34:56:00:f1:62",
148 | "device_id": "12:34:56:00:f1:62",
149 | "category": "human",
150 | "snapshot": {
151 | "id": "532dde8d17554c022ab071b9",
152 | "version": 1,
153 | "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
154 | "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
155 | },
156 | "vignette": {
157 | "id": "5dc021b5dea854bd2321707a",
158 | "version": 1,
159 | "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944",
160 | "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944"
161 | },
162 | "video_id": "12345678-1234-46cb-ad8f-23d89387409a",
163 | "video_status": "available",
164 | "message": "Bewegung erkannt"
165 | }
166 | ]
167 | }
168 | ],
169 | "user": {
170 | "reg_locale": "de-DE",
171 | "lang": "de-DE",
172 | "country": "DE",
173 | "mail": "john@doe.com"
174 | },
175 | "global_info": {
176 | "show_tags": true
177 | }
178 | },
179 | "status": "ok",
180 | "time_exec": 0.03621506690979,
181 | "time_server": 1560626960
182 | }
--------------------------------------------------------------------------------
/fixtures/camera_home_data_no_homes.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [],
4 | "user": {
5 | "reg_locale": "de-DE",
6 | "lang": "de-DE",
7 | "country": "DE",
8 | "mail": "john@doe.com"
9 | },
10 | "global_info": {
11 | "show_tags": true
12 | }
13 | },
14 | "status": "ok",
15 | "time_exec": 0.03621506690979,
16 | "time_server": 1560626960
17 | }
--------------------------------------------------------------------------------
/fixtures/camera_image_sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jabesq/netatmo-api-python/42848d146b9fee922790c10c24ee3fd94aab1ac2/fixtures/camera_image_sample.jpg
--------------------------------------------------------------------------------
/fixtures/camera_ping.json:
--------------------------------------------------------------------------------
1 | {
2 | "local_url": "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d",
3 | "product_name": "Welcome Netatmo"
4 | }
--------------------------------------------------------------------------------
/fixtures/camera_set_state_error.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 21,
4 | "message": "Invalid device_id, 12:34:56:00:f1:ff"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/camera_set_state_error_already_on.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1582932399,
4 | "body": {
5 | "home": {
6 | "id": "91763b24c43d3e344f424e8b"
7 | },
8 | "errors": [
9 | {
10 | "code": 23,
11 | "message": "Already on",
12 | "id": "12:34:56:00:f1:62",
13 | "command": "command/changestatus"
14 | }
15 | ]
16 | }
17 | }
--------------------------------------------------------------------------------
/fixtures/camera_set_state_error_wrong_parameter.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 21,
4 | "message": "cannot set property floodlight for module 12:34:56:00:f1:62"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/camera_set_state_ok.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1582932411
4 | }
--------------------------------------------------------------------------------
/fixtures/error_scope.json:
--------------------------------------------------------------------------------
1 | {"error":{"code":13,"message":"Application does not have the good scope rights"}}
2 |
--------------------------------------------------------------------------------
/fixtures/home_coach_no_devices.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "devices": [],
4 | "user": {
5 | "mail": "john@doe.com",
6 | "administrative": {
7 | "lang": "de-DE",
8 | "reg_locale": "de-DE",
9 | "country": "DE",
10 | "unit": 0,
11 | "windunit": 0,
12 | "pressureunit": 0,
13 | "feel_like_algo": 0
14 | }
15 | }
16 | },
17 | "status": "ok",
18 | "time_exec": 0.05824708938598633,
19 | "time_server": 1565377059
20 | }
--------------------------------------------------------------------------------
/fixtures/home_coach_simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "devices": [
4 | {
5 | "_id": "12:34:56:26:69:0c",
6 | "cipher_id": "enc:16:1UqwQlYV5AY2pfyEi5H47dmmFOOL3mCUo+KAkchL4A2CLI5u0e45Xr5jeAswO+XO",
7 | "date_setup": 1544560184,
8 | "last_setup": 1544560184,
9 | "type": "NHC",
10 | "last_status_store": 1558268332,
11 | "firmware": 45,
12 | "last_upgrade": 1544560186,
13 | "wifi_status": 58,
14 | "reachable": false,
15 | "co2_calibrating": false,
16 | "station_name": "Bedroom",
17 | "data_type": [
18 | "Temperature",
19 | "CO2",
20 | "Humidity",
21 | "Noise",
22 | "Pressure",
23 | "health_idx"
24 | ],
25 | "place": {
26 | "city": "Frankfurt",
27 | "country": "DE",
28 | "timezone": "Europe/Berlin",
29 | "location": [
30 | 52.516263,
31 | 13.377726
32 | ]
33 | }
34 | },
35 | {
36 | "_id": "12:34:56:25:cf:a8",
37 | "cipher_id": "enc:16:A+Jm0yFWBwUyKinFDutPZK7I2PuHN1fqaE9oB/KF+McbFs3oN9CKpR/dYbqL4om2",
38 | "date_setup": 1544562192,
39 | "last_setup": 1544562192,
40 | "type": "NHC",
41 | "last_status_store": 1559198922,
42 | "firmware": 45,
43 | "last_upgrade": 1544562194,
44 | "wifi_status": 41,
45 | "reachable": true,
46 | "co2_calibrating": false,
47 | "station_name": "Kitchen",
48 | "data_type": [
49 | "Temperature",
50 | "CO2",
51 | "Humidity",
52 | "Noise",
53 | "Pressure",
54 | "health_idx"
55 | ],
56 | "place": {
57 | "city": "Frankfurt",
58 | "country": "DE",
59 | "timezone": "Europe/Berlin",
60 | "location": [
61 | 52.516263,
62 | 13.377726
63 | ]
64 | }
65 | },
66 | {
67 | "_id": "12:34:56:26:65:14",
68 | "cipher_id": "enc:16:7kK6ZzG4L7NgfZZ6+dMvNxw4l6vXu+88SEJkCUklNdPa4KYIHmsfa1moOilEK61i",
69 | "date_setup": 1544564061,
70 | "last_setup": 1544564061,
71 | "type": "NHC",
72 | "last_status_store": 1559067159,
73 | "firmware": 45,
74 | "last_upgrade": 1544564302,
75 | "wifi_status": 66,
76 | "reachable": true,
77 | "co2_calibrating": false,
78 | "station_name": "Livingroom",
79 | "data_type": [
80 | "Temperature",
81 | "CO2",
82 | "Humidity",
83 | "Noise",
84 | "Pressure",
85 | "health_idx"
86 | ],
87 | "place": {
88 | "city": "Frankfurt",
89 | "country": "DE",
90 | "timezone": "Europe/Berlin",
91 | "location": [
92 | 52.516263,
93 | 13.377726
94 | ]
95 | }
96 | },
97 | {
98 | "_id": "12:34:56:3e:c5:46",
99 | "station_name": "Parents Bedroom",
100 | "date_setup": 1570732241,
101 | "last_setup": 1570732241,
102 | "type": "NHC",
103 | "last_status_store": 1572073818,
104 | "module_name": "Indoor",
105 | "firmware": 45,
106 | "wifi_status": 67,
107 | "reachable": true,
108 | "co2_calibrating": false,
109 | "data_type": [
110 | "Temperature",
111 | "CO2",
112 | "Humidity",
113 | "Noise",
114 | "Pressure",
115 | "health_idx"
116 | ],
117 | "place": {
118 | "city": "Frankfurt",
119 | "country": "DE",
120 | "timezone": "Europe/Berlin",
121 | "location": [
122 | 52.516263,
123 | 13.377726
124 | ]
125 | },
126 | "dashboard_data": {
127 | "time_utc": 1572073816,
128 | "Temperature": 20.3,
129 | "CO2": 494,
130 | "Humidity": 63,
131 | "Noise": 42,
132 | "Pressure": 1014.5,
133 | "AbsolutePressure": 1004.1,
134 | "health_idx": 1,
135 | "min_temp": 20.3,
136 | "max_temp": 21.6,
137 | "date_max_temp": 1572059333,
138 | "date_min_temp": 1572073816
139 | }
140 | },
141 | {
142 | "_id": "12:34:56:26:68:92",
143 | "station_name": "Baby Bedroom",
144 | "date_setup": 1571342643,
145 | "last_setup": 1571342643,
146 | "type": "NHC",
147 | "last_status_store": 1572073995,
148 | "module_name": "Indoor",
149 | "firmware": 45,
150 | "wifi_status": 68,
151 | "reachable": true,
152 | "co2_calibrating": false,
153 | "data_type": [
154 | "Temperature",
155 | "CO2",
156 | "Humidity",
157 | "Noise",
158 | "Pressure",
159 | "health_idx"
160 | ],
161 | "place": {
162 | "city": "Frankfurt",
163 | "country": "DE",
164 | "timezone": "Europe/Berlin",
165 | "location": [
166 | 52.516263,
167 | 13.377726
168 | ]
169 | },
170 | "dashboard_data": {
171 | "time_utc": 1572073994,
172 | "Temperature": 21.6,
173 | "CO2": 1053,
174 | "Humidity": 66,
175 | "Noise": 45,
176 | "Pressure": 1021.4,
177 | "AbsolutePressure": 1011,
178 | "health_idx": 1,
179 | "min_temp": 20.9,
180 | "max_temp": 21.6,
181 | "date_max_temp": 1572073690,
182 | "date_min_temp": 1572064254
183 | }
184 | }
185 | ],
186 | "user": {
187 | "mail": "john@doe.com",
188 | "administrative": {
189 | "lang": "de-DE",
190 | "reg_locale": "de-DE",
191 | "country": "DE",
192 | "unit": 0,
193 | "windunit": 0,
194 | "pressureunit": 0,
195 | "feel_like_algo": 0
196 | }
197 | }
198 | },
199 | "status": "ok",
200 | "time_exec": 0.095954179763794,
201 | "time_server": 1559463229
202 | }
--------------------------------------------------------------------------------
/fixtures/home_data_empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {},
3 | "status": "ok",
4 | "time_exec": 0.056135892868042,
5 | "time_server": 1559171003
6 | }
--------------------------------------------------------------------------------
/fixtures/home_data_no_devices.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [
4 | {
5 | "id": "91763b24c43d3e344f424e8c",
6 | "altitude": 112,
7 | "coordinates": [
8 | 52.516263,
9 | 13.377726
10 | ],
11 | "country": "DE",
12 | "timezone": "Europe/Berlin",
13 | "therm_setpoint_default_duration": 180,
14 | "therm_mode": "schedule"
15 | }
16 | ],
17 | "user": {
18 | "email": "john@doe.com",
19 | "language": "de-DE",
20 | "locale": "de-DE",
21 | "feel_like_algorithm": 0,
22 | "unit_pressure": 0,
23 | "unit_system": 0,
24 | "unit_wind": 0,
25 | "id": "91763b24c43d3e344f424e8b"
26 | }
27 | },
28 | "status": "ok",
29 | "time_exec": 0.056135892868042,
30 | "time_server": 1559171003
31 | }
--------------------------------------------------------------------------------
/fixtures/home_data_no_homes.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [],
4 | "user": {
5 | "email": "john@doe.com",
6 | "language": "de-DE",
7 | "locale": "de-DE",
8 | "feel_like_algorithm": 0,
9 | "unit_pressure": 0,
10 | "unit_system": 0,
11 | "unit_wind": 0,
12 | "id": "91763b24c43d3e344f424e8b"
13 | }
14 | },
15 | "status": "ok",
16 | "time_exec": 0.056135892868042,
17 | "time_server": 1559171003
18 | }
--------------------------------------------------------------------------------
/fixtures/home_data_nohomename.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "homes": [
4 | {
5 | "id": "91763b24c43d3e344f424e8b",
6 | "altitude": 112,
7 | "coordinates": [
8 | 52.516263,
9 | 13.377726
10 | ],
11 | "country": "DE",
12 | "timezone": "Europe/Berlin",
13 | "rooms": [
14 | {
15 | "id": "2746182631",
16 | "name": "Livingroom",
17 | "type": "livingroom",
18 | "module_ids": [
19 | "12:34:56:00:01:ae"
20 | ]
21 | },
22 | {
23 | "id": "3688132631",
24 | "name": "Hall",
25 | "type": "custom",
26 | "module_ids": [
27 | "12:34:56:00:f1:62"
28 | ]
29 | }
30 | ],
31 | "modules": [
32 | {
33 | "id": "12:34:56:00:fa:d0",
34 | "type": "NAPlug",
35 | "name": "Thermostat",
36 | "setup_date": 1494963356,
37 | "modules_bridged": [
38 | "12:34:56:00:01:ae"
39 | ]
40 | },
41 | {
42 | "id": "12:34:56:00:01:ae",
43 | "type": "NATherm1",
44 | "name": "Livingroom",
45 | "setup_date": 1494963356,
46 | "room_id": "2746182631",
47 | "bridge": "12:34:56:00:fa:d0"
48 | },
49 | {
50 | "id": "12:34:56:00:f1:62",
51 | "type": "NACamera",
52 | "name": "Hall",
53 | "setup_date": 1544828430,
54 | "room_id": "3688132631"
55 | }
56 | ],
57 | "therm_schedules": [
58 | {
59 | "zones": [
60 | {
61 | "type": 0,
62 | "name": "Comfort",
63 | "rooms_temp": [
64 | {
65 | "temp": 21,
66 | "room_id": "2746182631"
67 | }
68 | ],
69 | "id": 0
70 | },
71 | {
72 | "type": 1,
73 | "name": "Night",
74 | "rooms_temp": [
75 | {
76 | "temp": 17,
77 | "room_id": "2746182631"
78 | }
79 | ],
80 | "id": 1
81 | },
82 | {
83 | "type": 5,
84 | "name": "Eco",
85 | "rooms_temp": [
86 | {
87 | "temp": 17,
88 | "room_id": "2746182631"
89 | }
90 | ],
91 | "id": 4
92 | }
93 | ],
94 | "timetable": [
95 | {
96 | "zone_id": 1,
97 | "m_offset": 0
98 | },
99 | {
100 | "zone_id": 0,
101 | "m_offset": 360
102 | },
103 | {
104 | "zone_id": 4,
105 | "m_offset": 420
106 | },
107 | {
108 | "zone_id": 0,
109 | "m_offset": 960
110 | },
111 | {
112 | "zone_id": 1,
113 | "m_offset": 1410
114 | },
115 | {
116 | "zone_id": 0,
117 | "m_offset": 1800
118 | },
119 | {
120 | "zone_id": 4,
121 | "m_offset": 1860
122 | },
123 | {
124 | "zone_id": 0,
125 | "m_offset": 2400
126 | },
127 | {
128 | "zone_id": 1,
129 | "m_offset": 2850
130 | },
131 | {
132 | "zone_id": 0,
133 | "m_offset": 3240
134 | },
135 | {
136 | "zone_id": 4,
137 | "m_offset": 3300
138 | },
139 | {
140 | "zone_id": 0,
141 | "m_offset": 3840
142 | },
143 | {
144 | "zone_id": 1,
145 | "m_offset": 4290
146 | },
147 | {
148 | "zone_id": 0,
149 | "m_offset": 4680
150 | },
151 | {
152 | "zone_id": 4,
153 | "m_offset": 4740
154 | },
155 | {
156 | "zone_id": 0,
157 | "m_offset": 5280
158 | },
159 | {
160 | "zone_id": 1,
161 | "m_offset": 5730
162 | },
163 | {
164 | "zone_id": 0,
165 | "m_offset": 6120
166 | },
167 | {
168 | "zone_id": 4,
169 | "m_offset": 6180
170 | },
171 | {
172 | "zone_id": 0,
173 | "m_offset": 6720
174 | },
175 | {
176 | "zone_id": 1,
177 | "m_offset": 7170
178 | },
179 | {
180 | "zone_id": 0,
181 | "m_offset": 7620
182 | },
183 | {
184 | "zone_id": 1,
185 | "m_offset": 8610
186 | },
187 | {
188 | "zone_id": 0,
189 | "m_offset": 9060
190 | },
191 | {
192 | "zone_id": 1,
193 | "m_offset": 10050
194 | }
195 | ],
196 | "hg_temp": 7,
197 | "away_temp": 14,
198 | "name": "Default",
199 | "selected": true,
200 | "id": "591b54a2764ff4d50d8b5795",
201 | "type": "therm"
202 | }
203 | ],
204 | "therm_setpoint_default_duration": 120,
205 | "persons": [
206 | {
207 | "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
208 | "pseudo": "John Doe",
209 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
210 | },
211 | {
212 | "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
213 | "pseudo": "Jane Doe",
214 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
215 | },
216 | {
217 | "id": "91827376-7e04-5298-83af-a0cb8372dff3",
218 | "pseudo": "Richard Doe",
219 | "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
220 | }
221 | ],
222 | "schedules": [
223 | {
224 | "zones": [
225 | {
226 | "type": 0,
227 | "name": "Komfort",
228 | "rooms_temp": [
229 | {
230 | "temp": 21,
231 | "room_id": "2746182631"
232 | }
233 | ],
234 | "id": 0,
235 | "rooms": [
236 | {
237 | "id": "2746182631",
238 | "therm_setpoint_temperature": 21
239 | }
240 | ]
241 | },
242 | {
243 | "type": 1,
244 | "name": "Nacht",
245 | "rooms_temp": [
246 | {
247 | "temp": 17,
248 | "room_id": "2746182631"
249 | }
250 | ],
251 | "id": 1,
252 | "rooms": [
253 | {
254 | "id": "2746182631",
255 | "therm_setpoint_temperature": 17
256 | }
257 | ]
258 | },
259 | {
260 | "type": 5,
261 | "name": "Eco",
262 | "rooms_temp": [
263 | {
264 | "temp": 17,
265 | "room_id": "2746182631"
266 | }
267 | ],
268 | "id": 4,
269 | "rooms": [
270 | {
271 | "id": "2746182631",
272 | "therm_setpoint_temperature": 17
273 | }
274 | ]
275 | }
276 | ],
277 | "timetable": [
278 | {
279 | "zone_id": 1,
280 | "m_offset": 0
281 | },
282 | {
283 | "zone_id": 0,
284 | "m_offset": 360
285 | },
286 | {
287 | "zone_id": 4,
288 | "m_offset": 420
289 | },
290 | {
291 | "zone_id": 0,
292 | "m_offset": 960
293 | },
294 | {
295 | "zone_id": 1,
296 | "m_offset": 1410
297 | },
298 | {
299 | "zone_id": 0,
300 | "m_offset": 1800
301 | },
302 | {
303 | "zone_id": 4,
304 | "m_offset": 1860
305 | },
306 | {
307 | "zone_id": 0,
308 | "m_offset": 2400
309 | },
310 | {
311 | "zone_id": 1,
312 | "m_offset": 2850
313 | },
314 | {
315 | "zone_id": 0,
316 | "m_offset": 3240
317 | },
318 | {
319 | "zone_id": 4,
320 | "m_offset": 3300
321 | },
322 | {
323 | "zone_id": 0,
324 | "m_offset": 3840
325 | },
326 | {
327 | "zone_id": 1,
328 | "m_offset": 4290
329 | },
330 | {
331 | "zone_id": 0,
332 | "m_offset": 4680
333 | },
334 | {
335 | "zone_id": 4,
336 | "m_offset": 4740
337 | },
338 | {
339 | "zone_id": 0,
340 | "m_offset": 5280
341 | },
342 | {
343 | "zone_id": 1,
344 | "m_offset": 5730
345 | },
346 | {
347 | "zone_id": 0,
348 | "m_offset": 6120
349 | },
350 | {
351 | "zone_id": 4,
352 | "m_offset": 6180
353 | },
354 | {
355 | "zone_id": 0,
356 | "m_offset": 6720
357 | },
358 | {
359 | "zone_id": 1,
360 | "m_offset": 7170
361 | },
362 | {
363 | "zone_id": 0,
364 | "m_offset": 7620
365 | },
366 | {
367 | "zone_id": 1,
368 | "m_offset": 8610
369 | },
370 | {
371 | "zone_id": 0,
372 | "m_offset": 9060
373 | },
374 | {
375 | "zone_id": 1,
376 | "m_offset": 10050
377 | }
378 | ],
379 | "hg_temp": 7,
380 | "away_temp": 14,
381 | "name": "Default",
382 | "id": "591b54a2764ff4d50d8b5795",
383 | "selected": true,
384 | "type": "therm"
385 | }
386 | ],
387 | "therm_mode": "schedule"
388 | }
389 | ],
390 | "user": {
391 | "email": "john@doe.com",
392 | "language": "de-DE",
393 | "locale": "de-DE",
394 | "feel_like_algorithm": 0,
395 | "unit_pressure": 0,
396 | "unit_system": 0,
397 | "unit_wind": 0,
398 | "id": "91763b24c43d3e344f424e8b"
399 | }
400 | },
401 | "status": "ok",
402 | "time_exec": 0.056135892868042,
403 | "time_server": 1559171003
404 | }
--------------------------------------------------------------------------------
/fixtures/home_status_empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1559292039,
4 | "body": {}
5 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_and_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1559292039,
4 | "body": {
5 | "home": {
6 | "modules": [
7 | {
8 | "id": "12:34:56:00:fa:d0",
9 | "type": "NAPlug",
10 | "firmware_revision": 174,
11 | "rf_strength": 107,
12 | "wifi_strength": 42
13 | },
14 | {
15 | "id": "12:34:56:00:01:ae",
16 | "reachable": true,
17 | "type": "NATherm1",
18 | "firmware_revision": 65,
19 | "rf_strength": 58,
20 | "battery_level": 3793,
21 | "boiler_valve_comfort_boost": false,
22 | "boiler_status": false,
23 | "anticipating": false,
24 | "bridge": "12:34:56:00:fa:d0",
25 | "battery_state": "high"
26 | },
27 | {
28 | "id": "12:34:56:03:a5:54",
29 | "reachable": true,
30 | "type": "NRV",
31 | "firmware_revision": 79,
32 | "rf_strength": 51,
33 | "battery_level": 3025,
34 | "bridge": "12:34:56:00:fa:d0",
35 | "battery_state": "full"
36 | },
37 | {
38 | "id": "12:34:56:03:a0:ac",
39 | "reachable": true,
40 | "type": "NRV",
41 | "firmware_revision": 79,
42 | "rf_strength": 59,
43 | "battery_level": 3029,
44 | "bridge": "12:34:56:00:fa:d0",
45 | "battery_state": "full"
46 | }
47 | ],
48 | "rooms": [
49 | {
50 | "id": "2746182631",
51 | "reachable": true,
52 | "therm_measured_temperature": 19.8,
53 | "therm_setpoint_temperature": 12,
54 | "therm_setpoint_mode": "away",
55 | "therm_setpoint_start_time": 1559229567,
56 | "therm_setpoint_end_time": 0
57 | },
58 | {
59 | "id": "2940411577",
60 | "reachable": true,
61 | "therm_measured_temperature": 27,
62 | "heating_power_request": 0,
63 | "therm_setpoint_temperature": 7,
64 | "therm_setpoint_mode": "hg",
65 | "therm_setpoint_start_time": 0,
66 | "therm_setpoint_end_time": 0,
67 | "anticipating": false,
68 | "open_window": false
69 | },
70 | {
71 | "id": "2833524037",
72 | "reachable": true,
73 | "therm_measured_temperature": 24.5,
74 | "heating_power_request": 0,
75 | "therm_setpoint_temperature": 7,
76 | "therm_setpoint_mode": "hg",
77 | "therm_setpoint_start_time": 0,
78 | "therm_setpoint_end_time": 0,
79 | "anticipating": false,
80 | "open_window": false
81 | }
82 | ],
83 | "id": "91763b24c43d3e344f424e8b",
84 | "persons": [
85 | {
86 | "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
87 | "last_seen": 1557071156,
88 | "out_of_sight": true
89 | },
90 | {
91 | "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
92 | "last_seen": 1559282761,
93 | "out_of_sight": false
94 | },
95 | {
96 | "id": "91827376-7e04-5298-83af-a0cb8372dff3",
97 | "last_seen": 1559224132,
98 | "out_of_sight": true
99 | }
100 | ]
101 | },
102 | "errors": [
103 | {
104 | "code": 6,
105 | "id": "12:34:56:00:f1:62"
106 | }
107 | ]
108 | }
109 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_disconnected.json:
--------------------------------------------------------------------------------
1 | {
2 | "status":"ok",
3 | "body":{
4 | "errors":[
5 | {
6 | "code":6,
7 | "id":"12:34:56:00:fa:d0"
8 | }
9 | ],
10 | "home":{
11 | "id":"12:34:56:00:f1:62"
12 | }
13 | },
14 | "time_server":1559292039
15 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_invalid_id.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 21,
4 | "message": "Invalid id"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_invalid_schedule_id.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 21,
4 | "message": "schedule is not therm schedule"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_missing_home_id.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 10,
4 | "message": "Missing home_id"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_missing_parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 10,
4 | "message": "Missing parameters"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_mode_is_missing.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 10,
4 | "message": "mode is missing"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_error_mode_not_authorized.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "code": 21,
4 | "message": "mode not authorized"
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/home_status_simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1559292039,
4 | "body": {
5 | "home": {
6 | "modules": [
7 | {
8 | "id": "12:34:56:00:f1:62",
9 | "type": "NACamera",
10 | "monitoring": "on",
11 | "sd_status": 4,
12 | "alim_status": 2,
13 | "locked": false,
14 | "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,",
15 | "is_local": true
16 | },
17 | {
18 | "id": "12:34:56:00:fa:d0",
19 | "type": "NAPlug",
20 | "firmware_revision": 174,
21 | "rf_strength": 107,
22 | "wifi_strength": 42
23 | },
24 | {
25 | "id": "12:34:56:00:01:ae",
26 | "reachable": true,
27 | "type": "NATherm1",
28 | "firmware_revision": 65,
29 | "rf_strength": 58,
30 | "battery_level": 3793,
31 | "boiler_valve_comfort_boost": false,
32 | "boiler_status": false,
33 | "anticipating": false,
34 | "bridge": "12:34:56:00:fa:d0",
35 | "battery_state": "high"
36 | },
37 | {
38 | "id": "12:34:56:03:a5:54",
39 | "reachable": true,
40 | "type": "NRV",
41 | "firmware_revision": 79,
42 | "rf_strength": 51,
43 | "battery_level": 3025,
44 | "bridge": "12:34:56:00:fa:d0",
45 | "battery_state": "full"
46 | },
47 | {
48 | "id": "12:34:56:03:a0:ac",
49 | "reachable": true,
50 | "type": "NRV",
51 | "firmware_revision": 79,
52 | "rf_strength": 59,
53 | "battery_level": 3029,
54 | "bridge": "12:34:56:00:fa:d0",
55 | "battery_state": "full"
56 | }
57 | ],
58 | "rooms": [
59 | {
60 | "id": "2746182631",
61 | "reachable": true,
62 | "therm_measured_temperature": 19.8,
63 | "therm_setpoint_temperature": 12,
64 | "therm_setpoint_mode": "away",
65 | "therm_setpoint_start_time": 1559229567,
66 | "therm_setpoint_end_time": 0
67 | },
68 | {
69 | "id": "2940411577",
70 | "reachable": true,
71 | "therm_measured_temperature": 27,
72 | "heating_power_request": 0,
73 | "therm_setpoint_temperature": 7,
74 | "therm_setpoint_mode": "hg",
75 | "therm_setpoint_start_time": 0,
76 | "therm_setpoint_end_time": 0,
77 | "anticipating": false,
78 | "open_window": false
79 | },
80 | {
81 | "id": "2833524037",
82 | "reachable": true,
83 | "therm_measured_temperature": 24.5,
84 | "heating_power_request": 0,
85 | "therm_setpoint_temperature": 7,
86 | "therm_setpoint_mode": "hg",
87 | "therm_setpoint_start_time": 0,
88 | "therm_setpoint_end_time": 0,
89 | "anticipating": false,
90 | "open_window": false
91 | }
92 | ],
93 | "id": "91763b24c43d3e344f424e8b",
94 | "persons": [
95 | {
96 | "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
97 | "last_seen": 1557071156,
98 | "out_of_sight": true
99 | },
100 | {
101 | "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
102 | "last_seen": 1559282761,
103 | "out_of_sight": false
104 | },
105 | {
106 | "id": "91827376-7e04-5298-83af-a0cb8372dff3",
107 | "last_seen": 1559224132,
108 | "out_of_sight": true
109 | }
110 | ]
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/fixtures/invalid_grant.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": "invalid_grant"
3 | }
--------------------------------------------------------------------------------
/fixtures/oauth2_token.json:
--------------------------------------------------------------------------------
1 | {
2 | "access_token": "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12",
3 | "refresh_token": "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93",
4 | "scope": [
5 | "read_station",
6 | "read_camera",
7 | "access_camera",
8 | "read_thermostat",
9 | "write_thermostat",
10 | "read_presence",
11 | "access_presence"
12 | ],
13 | "expires_in": 10800,
14 | "expire_in": 10800
15 | }
--------------------------------------------------------------------------------
/fixtures/public_data_error_mongo.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": {
3 | "message": "failed to connect to server [localhost:27020] on first connect [MongoError: connect ECONNREFUSED 127.0.0.1:27020]",
4 | "code": 0
5 | }
6 | }
--------------------------------------------------------------------------------
/fixtures/public_data_simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_server": 1560248397,
4 | "time_exec": 0,
5 | "body": [
6 | {
7 | "_id": "70:ee:50:36:94:7c",
8 | "place": {
9 | "location": [
10 | 8.791382999999996,
11 | 50.2136394
12 | ],
13 | "timezone": "Europe/Berlin",
14 | "country": "DE",
15 | "altitude": 132
16 | },
17 | "mark": 14,
18 | "measures": {
19 | "02:00:00:36:f2:94": {
20 | "res": {
21 | "1560248022": [
22 | 21.4,
23 | 62
24 | ]
25 | },
26 | "type": [
27 | "temperature",
28 | "humidity"
29 | ]
30 | },
31 | "70:ee:50:36:94:7c": {
32 | "res": {
33 | "1560248030": [
34 | 1010.6
35 | ]
36 | },
37 | "type": [
38 | "pressure"
39 | ]
40 | },
41 | "05:00:00:05:33:84": {
42 | "rain_60min": 0.2,
43 | "rain_24h": 12.322000000000001,
44 | "rain_live": 0.5,
45 | "rain_timeutc": 1560248022
46 | }
47 | },
48 | "modules": [
49 | "05:00:00:05:33:84",
50 | "02:00:00:36:f2:94"
51 | ],
52 | "module_types": {
53 | "05:00:00:05:33:84": "NAModule3",
54 | "02:00:00:36:f2:94": "NAModule1"
55 | }
56 | },
57 | {
58 | "_id": "70:ee:50:1f:68:9e",
59 | "place": {
60 | "location": [
61 | 8.795445200000017,
62 | 50.2130169
63 | ],
64 | "timezone": "Europe/Berlin",
65 | "country": "DE",
66 | "altitude": 125
67 | },
68 | "mark": 14,
69 | "measures": {
70 | "02:00:00:1f:82:28": {
71 | "res": {
72 | "1560248312": [
73 | 21.1,
74 | 69
75 | ]
76 | },
77 | "type": [
78 | "temperature",
79 | "humidity"
80 | ]
81 | },
82 | "70:ee:50:1f:68:9e": {
83 | "res": {
84 | "1560248344": [
85 | 1007.3
86 | ]
87 | },
88 | "type": [
89 | "pressure"
90 | ]
91 | },
92 | "05:00:00:02:bb:6e": {
93 | "rain_60min": 0,
94 | "rain_24h": 9.999,
95 | "rain_live": 0,
96 | "rain_timeutc": 1560248344
97 | }
98 | },
99 | "modules": [
100 | "02:00:00:1f:82:28",
101 | "05:00:00:02:bb:6e"
102 | ],
103 | "module_types": {
104 | "02:00:00:1f:82:28": "NAModule1",
105 | "05:00:00:02:bb:6e": "NAModule3"
106 | }
107 | },
108 | {
109 | "_id": "70:ee:50:27:25:b0",
110 | "place": {
111 | "location": [
112 | 8.7807159,
113 | 50.1946167
114 | ],
115 | "timezone": "Europe/Berlin",
116 | "country": "DE",
117 | "altitude": 112
118 | },
119 | "mark": 14,
120 | "measures": {
121 | "02:00:00:27:19:b2": {
122 | "res": {
123 | "1560247889": [
124 | 23.2,
125 | 60
126 | ]
127 | },
128 | "type": [
129 | "temperature",
130 | "humidity"
131 | ]
132 | },
133 | "70:ee:50:27:25:b0": {
134 | "res": {
135 | "1560247907": [
136 | 1012.8
137 | ]
138 | },
139 | "type": [
140 | "pressure"
141 | ]
142 | },
143 | "05:00:00:03:5d:2e": {
144 | "rain_60min": 0,
145 | "rain_24h": 11.716000000000001,
146 | "rain_live": 0,
147 | "rain_timeutc": 1560247896
148 | }
149 | },
150 | "modules": [
151 | "02:00:00:27:19:b2",
152 | "05:00:00:03:5d:2e"
153 | ],
154 | "module_types": {
155 | "02:00:00:27:19:b2": "NAModule1",
156 | "05:00:00:03:5d:2e": "NAModule3"
157 | }
158 | },
159 | {
160 | "_id": "70:ee:50:04:ed:7a",
161 | "place": {
162 | "location": [
163 | 8.785034,
164 | 50.192169
165 | ],
166 | "timezone": "Europe/Berlin",
167 | "country": "DE",
168 | "altitude": 112
169 | },
170 | "mark": 14,
171 | "measures": {
172 | "02:00:00:04:c2:2e": {
173 | "res": {
174 | "1560248137": [
175 | 19.8,
176 | 76
177 | ]
178 | },
179 | "type": [
180 | "temperature",
181 | "humidity"
182 | ]
183 | },
184 | "70:ee:50:04:ed:7a": {
185 | "res": {
186 | "1560248152": [
187 | 1005.4
188 | ]
189 | },
190 | "type": [
191 | "pressure"
192 | ]
193 | }
194 | },
195 | "modules": [
196 | "02:00:00:04:c2:2e"
197 | ],
198 | "module_types": {
199 | "02:00:00:04:c2:2e": "NAModule1"
200 | }
201 | },
202 | {
203 | "_id": "70:ee:50:27:9f:2c",
204 | "place": {
205 | "location": [
206 | 8.785342,
207 | 50.193573
208 | ],
209 | "timezone": "Europe/Berlin",
210 | "country": "DE",
211 | "altitude": 116
212 | },
213 | "mark": 1,
214 | "measures": {
215 | "02:00:00:27:aa:70": {
216 | "res": {
217 | "1560247821": [
218 | 25.5,
219 | 56
220 | ]
221 | },
222 | "type": [
223 | "temperature",
224 | "humidity"
225 | ]
226 | },
227 | "70:ee:50:27:9f:2c": {
228 | "res": {
229 | "1560247853": [
230 | 1010.6
231 | ]
232 | },
233 | "type": [
234 | "pressure"
235 | ]
236 | }
237 | },
238 | "modules": [
239 | "02:00:00:27:aa:70"
240 | ],
241 | "module_types": {
242 | "02:00:00:27:aa:70": "NAModule1"
243 | }
244 | },
245 | {
246 | "_id": "70:ee:50:01:20:fa",
247 | "place": {
248 | "location": [
249 | 8.7953,
250 | 50.195241
251 | ],
252 | "timezone": "Europe/Berlin",
253 | "country": "DE",
254 | "altitude": 119
255 | },
256 | "mark": 1,
257 | "measures": {
258 | "02:00:00:00:f7:ba": {
259 | "res": {
260 | "1560247831": [
261 | 27.4,
262 | 58
263 | ]
264 | },
265 | "type": [
266 | "temperature",
267 | "humidity"
268 | ]
269 | },
270 | "70:ee:50:01:20:fa": {
271 | "res": {
272 | "1560247876": [
273 | 1014.4
274 | ]
275 | },
276 | "type": [
277 | "pressure"
278 | ]
279 | }
280 | },
281 | "modules": [
282 | "02:00:00:00:f7:ba"
283 | ],
284 | "module_types": {
285 | "02:00:00:00:f7:ba": "NAModule1"
286 | }
287 | },
288 | {
289 | "_id": "70:ee:50:3c:02:78",
290 | "place": {
291 | "location": [
292 | 8.795953681700666,
293 | 50.19530139868166
294 | ],
295 | "timezone": "Europe/Berlin",
296 | "country": "DE",
297 | "altitude": 119
298 | },
299 | "mark": 7,
300 | "measures": {
301 | "02:00:00:3c:21:f2": {
302 | "res": {
303 | "1560248225": [
304 | 23.3,
305 | 58
306 | ]
307 | },
308 | "type": [
309 | "temperature",
310 | "humidity"
311 | ]
312 | },
313 | "70:ee:50:3c:02:78": {
314 | "res": {
315 | "1560248270": [
316 | 1011.7
317 | ]
318 | },
319 | "type": [
320 | "pressure"
321 | ]
322 | }
323 | },
324 | "modules": [
325 | "02:00:00:3c:21:f2"
326 | ],
327 | "module_types": {
328 | "02:00:00:3c:21:f2": "NAModule1"
329 | }
330 | },
331 | {
332 | "_id": "70:ee:50:36:a9:fc",
333 | "place": {
334 | "location": [
335 | 8.801164269110814,
336 | 50.19596181704958
337 | ],
338 | "timezone": "Europe/Berlin",
339 | "country": "DE",
340 | "altitude": 113
341 | },
342 | "mark": 14,
343 | "measures": {
344 | "02:00:00:36:a9:50": {
345 | "res": {
346 | "1560248145": [
347 | 20.1,
348 | 67
349 | ]
350 | },
351 | "type": [
352 | "temperature",
353 | "humidity"
354 | ]
355 | },
356 | "70:ee:50:36:a9:fc": {
357 | "res": {
358 | "1560248191": [
359 | 1010
360 | ]
361 | },
362 | "type": [
363 | "pressure"
364 | ]
365 | },
366 | "05:00:00:02:92:82": {
367 | "rain_60min": 0,
368 | "rain_24h": 11.009,
369 | "rain_live": 0,
370 | "rain_timeutc": 1560248184
371 | },
372 | "06:00:00:03:19:76": {
373 | "wind_strength": 15,
374 | "wind_angle": 17,
375 | "gust_strength": 31,
376 | "gust_angle": 217,
377 | "wind_timeutc": 1560248190
378 | }
379 | },
380 | "modules": [
381 | "05:00:00:02:92:82",
382 | "02:00:00:36:a9:50",
383 | "06:00:00:03:19:76"
384 | ],
385 | "module_types": {
386 | "05:00:00:02:92:82": "NAModule3",
387 | "02:00:00:36:a9:50": "NAModule1",
388 | "06:00:00:03:19:76": "NAModule2"
389 | }
390 | }
391 | ]
392 | }
--------------------------------------------------------------------------------
/fixtures/status_ok.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "ok",
3 | "time_exec": 0.020781993865967,
4 | "time_server": 1559162635
5 | }
--------------------------------------------------------------------------------
/fixtures/thermostat_data_simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "devices": [
4 | {
5 | "_id": "12:34:56:00:fa:d0",
6 | "firmware": 174,
7 | "last_bilan": {
8 | "y": 2019,
9 | "m": 4
10 | },
11 | "last_setup": 1494963356,
12 | "last_status_store": 1559297986,
13 | "place": {
14 | "altitude": 112,
15 | "city": "Berlin",
16 | "country": "DE",
17 | "improveLocProposed": true,
18 | "location": [
19 | 52.516263,
20 | 13.377726
21 | ],
22 | "timezone": "Europe/Berlin",
23 | "trust_location": true
24 | },
25 | "plug_connected_boiler": 1,
26 | "type": "NAPlug",
27 | "udp_conn": true,
28 | "wifi_status": 42,
29 | "modules": [
30 | {
31 | "_id": "12:34:56:00:01:ae",
32 | "module_name": "Livingroom",
33 | "type": "NATherm1",
34 | "firmware": 65,
35 | "last_message": 1559297976,
36 | "rf_status": 59,
37 | "battery_vp": 3798,
38 | "therm_orientation": 3,
39 | "therm_relay_cmd": 0,
40 | "anticipating": false,
41 | "battery_percent": 53,
42 | "event_history": {
43 | "boiler_not_responding_events": [
44 | {
45 | "K": 1506103090
46 | },
47 | {
48 | "K": 1514496738
49 | },
50 | {
51 | "K": 1514583682
52 | },
53 | {
54 | "K": 1518695843
55 | },
56 | {
57 | "K": 1518813960
58 | }
59 | ],
60 | "boiler_responding_events": [
61 | {
62 | "K": 1506281109
63 | },
64 | {
65 | "K": 1514552830
66 | },
67 | {
68 | "K": 1514757686
69 | },
70 | {
71 | "K": 1518798339
72 | },
73 | {
74 | "K": 1518965265
75 | }
76 | ]
77 | },
78 | "setpoint_history": [
79 | {
80 | "setpoint": {
81 | "setpoint_mode": "hg"
82 | },
83 | "timestamp": 1559229554
84 | },
85 | {
86 | "setpoint": {
87 | "setpoint_mode": "program"
88 | },
89 | "timestamp": 1559229565
90 | },
91 | {
92 | "setpoint": {
93 | "setpoint_mode": "away"
94 | },
95 | "timestamp": 1559229567
96 | }
97 | ],
98 | "last_therm_seen": 1559297976,
99 | "setpoint": {
100 | "setpoint_mode": "away"
101 | },
102 | "therm_program_list": [
103 | {
104 | "zones": [
105 | {
106 | "type": 0,
107 | "name": "Comfort",
108 | "id": 0,
109 | "temp": 21
110 | },
111 | {
112 | "type": 1,
113 | "name": "Night",
114 | "id": 1,
115 | "temp": 17
116 | },
117 | {
118 | "type": 5,
119 | "name": "Eco",
120 | "id": 4,
121 | "temp": 17
122 | },
123 | {
124 | "type": 2,
125 | "id": 2,
126 | "temp": 14
127 | },
128 | {
129 | "type": 3,
130 | "id": 3,
131 | "temp": 7
132 | }
133 | ],
134 | "timetable": [
135 | {
136 | "m_offset": 0,
137 | "id": 1
138 | },
139 | {
140 | "m_offset": 360,
141 | "id": 0
142 | },
143 | {
144 | "m_offset": 420,
145 | "id": 4
146 | },
147 | {
148 | "m_offset": 960,
149 | "id": 0
150 | },
151 | {
152 | "m_offset": 1410,
153 | "id": 1
154 | },
155 | {
156 | "m_offset": 1800,
157 | "id": 0
158 | },
159 | {
160 | "m_offset": 1860,
161 | "id": 4
162 | },
163 | {
164 | "m_offset": 2400,
165 | "id": 0
166 | },
167 | {
168 | "m_offset": 2850,
169 | "id": 1
170 | },
171 | {
172 | "m_offset": 3240,
173 | "id": 0
174 | },
175 | {
176 | "m_offset": 3300,
177 | "id": 4
178 | },
179 | {
180 | "m_offset": 3840,
181 | "id": 0
182 | },
183 | {
184 | "m_offset": 4290,
185 | "id": 1
186 | },
187 | {
188 | "m_offset": 4680,
189 | "id": 0
190 | },
191 | {
192 | "m_offset": 4740,
193 | "id": 4
194 | },
195 | {
196 | "m_offset": 5280,
197 | "id": 0
198 | },
199 | {
200 | "m_offset": 5730,
201 | "id": 1
202 | },
203 | {
204 | "m_offset": 6120,
205 | "id": 0
206 | },
207 | {
208 | "m_offset": 6180,
209 | "id": 4
210 | },
211 | {
212 | "m_offset": 6720,
213 | "id": 0
214 | },
215 | {
216 | "m_offset": 7170,
217 | "id": 1
218 | },
219 | {
220 | "m_offset": 7620,
221 | "id": 0
222 | },
223 | {
224 | "m_offset": 8610,
225 | "id": 1
226 | },
227 | {
228 | "m_offset": 9060,
229 | "id": 0
230 | },
231 | {
232 | "m_offset": 10050,
233 | "id": 1
234 | }
235 | ],
236 | "name": "Default",
237 | "program_id": "591b54a2764ff4d50d8b5795",
238 | "selected": true
239 | }
240 | ],
241 | "measured": {
242 | "time": 1559297836,
243 | "temperature": 19.8,
244 | "setpoint_temp": 12
245 | }
246 | }
247 | ],
248 | "station_name": "Thermostat",
249 | "last_plug_seen": 1559297986
250 | }
251 | ],
252 | "user": {
253 | "mail": "john@doe.com",
254 | "administrative": {
255 | "lang": "de-DE",
256 | "reg_locale": "de-DE",
257 | "country": "DE",
258 | "unit": 0,
259 | "windunit": 0,
260 | "pressureunit": 0,
261 | "feel_like_algo": 0
262 | }
263 | }
264 | },
265 | "status": "ok",
266 | "time_exec": 0.12061500549316,
267 | "time_server": 1559300497
268 | }
--------------------------------------------------------------------------------
/fixtures/too_many_connections.json:
--------------------------------------------------------------------------------
1 | {
2 | "error": "too_many_connections"
3 | }
--------------------------------------------------------------------------------
/fixtures/weatherstation_data_unreachable_station.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "devices": [
4 | {
5 | "_id": "12:34:56:37:11:ca",
6 | "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX",
7 | "date_setup": 1544558432,
8 | "last_setup": 1544558432,
9 | "type": "NAMain",
10 | "last_status_store": 1559413181,
11 | "module_name": "NetatmoIndoor",
12 | "firmware": 137,
13 | "last_upgrade": 1544558433,
14 | "wifi_status": 45,
15 | "reachable": true,
16 | "co2_calibrating": false,
17 | "station_name": "MyStation",
18 | "data_type": [
19 | "Temperature",
20 | "CO2",
21 | "Humidity",
22 | "Noise",
23 | "Pressure"
24 | ],
25 | "place": {
26 | "altitude": 664,
27 | "city": "Frankfurt",
28 | "country": "DE",
29 | "timezone": "Europe/Berlin",
30 | "location": [
31 | 52.516263,
32 | 13.377726
33 | ]
34 | },
35 | "dashboard_data": {
36 | "time_utc": 1559413171,
37 | "Temperature": 24.6,
38 | "CO2": 749,
39 | "Humidity": 36,
40 | "Noise": 37,
41 | "Pressure": 1017.3,
42 | "AbsolutePressure": 939.7,
43 | "min_temp": 23.4,
44 | "max_temp": 25.6,
45 | "date_min_temp": 1559371924,
46 | "date_max_temp": 1559411964,
47 | "temp_trend": "stable",
48 | "pressure_trend": "down"
49 | },
50 | "modules": [
51 | {
52 | "_id": "12:34:56:36:fc:de",
53 | "type": "NAModule1",
54 | "module_name": "NetatmoOutdoor",
55 | "data_type": [
56 | "Temperature",
57 | "Humidity"
58 | ],
59 | "last_setup": 1544558433,
60 | "reachable": true,
61 | "dashboard_data": {
62 | "time_utc": 1559413157,
63 | "Temperature": 28.6,
64 | "Humidity": 24,
65 | "min_temp": 16.9,
66 | "max_temp": 30.3,
67 | "date_min_temp": 1559365579,
68 | "date_max_temp": 1559404698,
69 | "temp_trend": "down"
70 | },
71 | "firmware": 46,
72 | "last_message": 1559413177,
73 | "last_seen": 1559413157,
74 | "rf_status": 65,
75 | "battery_vp": 5738,
76 | "battery_percent": 87
77 | },
78 | {
79 | "_id": "12:34:56:07:bb:3e",
80 | "type": "NAModule4",
81 | "module_name": "Kitchen",
82 | "data_type": [
83 | "Temperature",
84 | "CO2",
85 | "Humidity"
86 | ],
87 | "last_setup": 1548956696,
88 | "reachable": true,
89 | "dashboard_data": {
90 | "time_utc": 1559413125,
91 | "Temperature": 28,
92 | "CO2": 503,
93 | "Humidity": 26,
94 | "min_temp": 25,
95 | "max_temp": 28,
96 | "date_min_temp": 1559371577,
97 | "date_max_temp": 1559412561,
98 | "temp_trend": "up"
99 | },
100 | "firmware": 44,
101 | "last_message": 1559413177,
102 | "last_seen": 1559413177,
103 | "rf_status": 73,
104 | "battery_vp": 5687,
105 | "battery_percent": 83
106 | },
107 | {
108 | "_id": "12:34:56:07:bb:0e",
109 | "type": "NAModule4",
110 | "module_name": "Livingroom",
111 | "data_type": [
112 | "Temperature",
113 | "CO2",
114 | "Humidity"
115 | ],
116 | "last_setup": 1548957209,
117 | "reachable": true,
118 | "dashboard_data": {
119 | "time_utc": 1559413093,
120 | "Temperature": 26.4,
121 | "CO2": 451,
122 | "Humidity": 31,
123 | "min_temp": 25.1,
124 | "max_temp": 26.4,
125 | "date_min_temp": 1559365290,
126 | "date_max_temp": 1559413093,
127 | "temp_trend": "stable"
128 | },
129 | "firmware": 44,
130 | "last_message": 1559413177,
131 | "last_seen": 1559413093,
132 | "rf_status": 84,
133 | "battery_vp": 5626,
134 | "battery_percent": 79
135 | },
136 | {
137 | "_id": "12:34:56:03:1b:e4",
138 | "type": "NAModule2",
139 | "module_name": "Garden",
140 | "data_type": [
141 | "Wind"
142 | ],
143 | "last_setup": 1549193862,
144 | "reachable": true,
145 | "dashboard_data": {
146 | "time_utc": 1559413170,
147 | "WindStrength": 4,
148 | "WindAngle": 217,
149 | "GustStrength": 9,
150 | "GustAngle": 206,
151 | "max_wind_str": 21,
152 | "max_wind_angle": 217,
153 | "date_max_wind_str": 1559386669
154 | },
155 | "firmware": 19,
156 | "last_message": 1559413177,
157 | "last_seen": 1559413177,
158 | "rf_status": 59,
159 | "battery_vp": 5689,
160 | "battery_percent": 85
161 | },
162 | {
163 | "_id": "12:34:56:05:51:20",
164 | "type": "NAModule3",
165 | "module_name": "Yard",
166 | "data_type": [
167 | "Rain"
168 | ],
169 | "last_setup": 1549194580,
170 | "reachable": true,
171 | "dashboard_data": {
172 | "time_utc": 1559413170,
173 | "Rain": 0,
174 | "sum_rain_24": 0,
175 | "sum_rain_1": 0
176 | },
177 | "firmware": 8,
178 | "last_message": 1559413177,
179 | "last_seen": 1559413170,
180 | "rf_status": 67,
181 | "battery_vp": 5860,
182 | "battery_percent": 93
183 | }
184 | ]
185 | },
186 | {
187 | "_id": "12:34:56:00:aa:01",
188 | "station_name": "MyRemoteStation",
189 | "date_setup": 1499189962,
190 | "last_setup": 1499189962,
191 | "type": "NAMain",
192 | "last_status_store": 1554506294,
193 | "module_name": "Indoor",
194 | "firmware": 132,
195 | "last_upgrade": 1499189915,
196 | "wifi_status": 46,
197 | "reachable": false,
198 | "co2_calibrating": false,
199 | "data_type": [
200 | "Temperature",
201 | "CO2",
202 | "Humidity",
203 | "Noise",
204 | "Pressure"
205 | ],
206 | "place": {
207 | "altitude": 6,
208 | "city": "Harstad",
209 | "country": "NO",
210 | "timezone": "Europe/Oslo",
211 | "location": [
212 | 59.895000,
213 | 10.620000
214 | ]
215 | },
216 | "modules": [
217 | {
218 | "_id": "12:34:56:00:aa:02",
219 | "type": "NAModule1",
220 | "module_name": "Outdoor",
221 | "data_type": [
222 | "Temperature",
223 | "Humidity"
224 | ],
225 | "last_setup": 1499189902,
226 | "battery_percent": 17,
227 | "reachable": false,
228 | "firmware": 44,
229 | "last_message": 1536805739,
230 | "last_seen": 1536696388,
231 | "rf_status": 87,
232 | "battery_vp": 4018,
233 | "main_device": "12:34:56:00:aa:01"
234 | },
235 | {
236 | "_id": "12:34:56:00:aa:03",
237 | "type": "NAModule2",
238 | "module_name": "Wind Gauge",
239 | "data_type": [
240 | "Wind"
241 | ],
242 | "last_setup": 1499190606,
243 | "battery_percent": 3,
244 | "reachable": false,
245 | "firmware": 18,
246 | "last_message": 1537259554,
247 | "last_seen": 1537259554,
248 | "rf_status": 74,
249 | "battery_vp": 4013,
250 | "main_device": "12:34:56:00:aa:01"
251 | }
252 | ]
253 | }
254 | ],
255 | "user": {
256 | "mail": "john@doe.com",
257 | "administrative": {
258 | "lang": "de-DE",
259 | "reg_locale": "de-DE",
260 | "country": "DE",
261 | "unit": 0,
262 | "windunit": 0,
263 | "pressureunit": 0,
264 | "feel_like_algo": 0
265 | }
266 | }
267 | },
268 | "status": "ok",
269 | "time_exec": 0.91107702255249,
270 | "time_server": 1559413602
271 | }
--------------------------------------------------------------------------------
/fixtures/weatherstation_measure.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": {
3 | "1544558433": [
4 | 28.1
5 | ],
6 | "1544558449": [
7 | 28.4
8 | ],
9 | "1544558504": [
10 | 27
11 | ],
12 | "1544558807": [
13 | 24
14 | ],
15 | "1544559062": [
16 | 23.8
17 | ],
18 | "1544559211": [
19 | 26.1
20 | ],
21 | "1544559308": [
22 | 24.9
23 | ],
24 | "1544559415": [
25 | 24.6
26 | ],
27 | "1544559576": [
28 | 24.2
29 | ],
30 | "1544559974": [
31 | 26.9
32 | ],
33 | "1544560021": [
34 | 27.1
35 | ],
36 | "1544560058": [
37 | 27.4
38 | ],
39 | "1544560361": [
40 | 26
41 | ]
42 | },
43 | "status": "ok",
44 | "time_exec": 0.33915495872498,
45 | "time_server": 1560590041
46 | }
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["wheel", "setuptools", "attrs>=17.1"]
3 | build-backend = "setuptools.build_meta"
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = pyatmo
3 | version = attr:version.__version__
4 | description = Simple API to access Netatmo weather station data from any Python 3 script. Designed for Home-Assitant (but not only)
5 | long_description = file: README.md
6 | long_description_content_type = text/markdown
7 | url = https://github.com/jabesq/netatmo-api-python
8 | author = Hugo Dupras
9 | author_email = jabesq@gmail.com
10 | license = MIT
11 | license_file = LICENSE.txt
12 | classifiers =
13 | License :: OSI Approved :: MIT License
14 | Programming Language :: Python :: 3
15 | Programming Language :: Python :: 3 :: Only
16 | Programming Language :: Python :: 3.7
17 | Programming Language :: Python :: 3.8
18 | Programming Language :: Python :: 3.9
19 | Programming Language :: Python :: Implementation :: CPython
20 | Programming Language :: Python :: Implementation :: PyPy
21 | Topic :: Home Automation
22 |
23 | [options]
24 | packages = find:
25 | py_modules = version
26 | install_requires =
27 | oauthlib~=3.1
28 | requests~=2.24
29 | requests_oauthlib~=1.3
30 | python_requires = ~=3.7
31 | package_dir = =src
32 |
33 | [options.packages.find]
34 | exclude = tests
35 | where = src
36 |
37 | [flake8]
38 | max-line-length = 88
39 | ignore = W503, E501
40 |
41 | [pep8]
42 | max-line-length = 88
43 | ignore = W503, E501
44 |
45 | [mypy]
46 | ignore_errors = True
47 | ignore_missing_imports = True
48 |
49 | [mypy-pyatmo.auth]
50 | ignore_errors = False
51 |
52 | [mypy-pyatmo.camera]
53 | ignore_errors = False
54 |
55 | [mypy-pyatmo.exceptions]
56 | ignore_errors = False
57 |
58 | [mypy-pyatmo.helpers]
59 | ignore_errors = False
60 |
61 | [isort]
62 | multi_line_output = 3
63 | include_trailing_comma = True
64 | force_grid_wrap = 0
65 | use_parentheses = True
66 | forced_separate = tests
67 | combine_as_imports = true
68 | line_length = 88
69 | skip_glob = venv
70 | known_third_party = freezegun,oauthlib,pytest,requests,requests_oauthlib,setuptools,tests
71 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
4 |
--------------------------------------------------------------------------------
/src/pyatmo/__init__.py:
--------------------------------------------------------------------------------
1 | from .auth import ClientAuth, NetatmoOAuth2
2 | from .camera import CameraData
3 | from .exceptions import ApiError, InvalidHome, InvalidRoom, NoDevice, NoSchedule
4 | from .home_coach import HomeCoachData
5 | from .public_data import PublicData
6 | from .thermostat import HomeData, HomeStatus
7 | from .weather_station import WeatherStationData
8 |
9 | __all__ = [
10 | "CameraData",
11 | "ClientAuth",
12 | "HomeCoachData",
13 | "HomeData",
14 | "HomeStatus",
15 | "InvalidHome",
16 | "InvalidRoom",
17 | "ApiError",
18 | "NetatmoOAuth2",
19 | "NoDevice",
20 | "NoSchedule",
21 | "PublicData",
22 | "WeatherStationData",
23 | ]
24 |
--------------------------------------------------------------------------------
/src/pyatmo/__main__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from pyatmo.auth import ALL_SCOPES, ClientAuth
5 | from pyatmo.camera import CameraData
6 | from pyatmo.exceptions import NoDevice
7 | from pyatmo.public_data import PublicData
8 | from pyatmo.thermostat import HomeData
9 | from pyatmo.weather_station import WeatherStationData
10 |
11 | LON_NE = 6.221652
12 | LAT_NE = 46.610870
13 | LON_SW = 6.217828
14 | LAT_SW = 46.596485
15 |
16 |
17 | def main():
18 | try:
19 | if (
20 | os.environ["CLIENT_ID"]
21 | and os.environ["CLIENT_SECRET"]
22 | and os.environ["USERNAME"]
23 | and os.environ["PASSWORD"]
24 | ):
25 | client_id = os.environ["CLIENT_ID"]
26 | client_secret = os.environ["CLIENT_SECRET"]
27 | username = os.environ["USERNAME"]
28 | password = os.environ["PASSWORD"]
29 | except KeyError:
30 | sys.stderr.write(
31 | "No credentials passed to pyatmo.py (client_id, client_secret, "
32 | "username, password)\n",
33 | )
34 | sys.exit(1)
35 |
36 | auth = ClientAuth(
37 | client_id=client_id,
38 | client_secret=client_secret,
39 | username=username,
40 | password=password,
41 | scope=" ".join(ALL_SCOPES),
42 | )
43 |
44 | try:
45 | WeatherStationData(auth)
46 | except NoDevice:
47 | if sys.stdout.isatty():
48 | print("pyatmo.py : warning, no weather station available for testing")
49 |
50 | try:
51 | CameraData(auth)
52 | except NoDevice:
53 | if sys.stdout.isatty():
54 | print("pyatmo.py : warning, no camera available for testing")
55 |
56 | try:
57 | HomeData(auth)
58 | except NoDevice:
59 | if sys.stdout.isatty():
60 | print("pyatmo.py : warning, no thermostat available for testing")
61 |
62 | PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW)
63 |
64 | # If we reach this line, all is OK
65 |
66 | # If launched interactively, display OK message
67 | if sys.stdout.isatty():
68 | print("pyatmo: OK")
69 |
70 | sys.exit(0)
71 |
72 |
73 | if __name__ == "__main__":
74 | main()
75 |
--------------------------------------------------------------------------------
/src/pyatmo/auth.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from json import JSONDecodeError
3 | from time import sleep
4 | from typing import Any, Callable, Dict, Optional, Tuple, Union
5 |
6 | import requests
7 | from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError
8 | from requests_oauthlib import OAuth2Session
9 |
10 | from pyatmo.exceptions import ApiError
11 | from pyatmo.helpers import _BASE_URL, ERRORS
12 |
13 | LOG = logging.getLogger(__name__)
14 |
15 | # Common definitions
16 | AUTH_REQ = _BASE_URL + "oauth2/token"
17 | AUTH_URL = _BASE_URL + "oauth2/authorize"
18 | WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook"
19 | WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook"
20 |
21 |
22 | # Possible scops
23 | ALL_SCOPES = [
24 | "read_station",
25 | "read_camera",
26 | "access_camera",
27 | "write_camera",
28 | "read_presence",
29 | "access_presence",
30 | "write_presence",
31 | "read_homecoach",
32 | "read_smokedetector",
33 | "read_thermostat",
34 | "write_thermostat",
35 | ]
36 |
37 |
38 | class NetatmoOAuth2:
39 | """
40 | Handle authentication with OAuth2
41 | """
42 |
43 | def __init__(
44 | self,
45 | client_id: str = None,
46 | client_secret: str = None,
47 | redirect_uri: Optional[str] = None,
48 | token: Optional[Dict[str, str]] = None,
49 | token_updater: Optional[Callable[[str], None]] = None,
50 | scope: Optional[str] = "read_station",
51 | ) -> None:
52 | """Initialize self.
53 |
54 | Keyword Arguments:
55 | client_id {str} -- Application client ID delivered by Netatmo on dev.netatmo.com (default: {None})
56 | client_secret {str} -- Application client secret delivered by Netatmo on dev.netatmo.com (default: {None})
57 | redirect_uri {Optional[str]} -- Redirect URI where to the authorization server will redirect with an authorization code (default: {None})
58 | token {Optional[Dict[str, str]]} -- Authorization token (default: {None})
59 | token_updater {Optional[Callable[[str], None]]} -- Callback when the token is updated (default: {None})
60 | scope {Optional[str]} -- List of scopes (default: {"read_station"})
61 | read_station: to retrieve weather station data (Getstationsdata, Getmeasure)
62 | read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture)
63 | access_camera: to access the camera, the videos and the live stream
64 | write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome)
65 | read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata)
66 | write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint)
67 | read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture)
68 | access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status
69 | read_homecoach: to retrieve Home Coache data (Gethomecoachsdata)
70 | read_smokedetector: to retrieve the smoke detector status (Gethomedata)
71 | Several values can be used at the same time, ie: 'read_station read_camera'
72 | """
73 | self.client_id = client_id
74 | self.client_secret = client_secret
75 | self.redirect_uri = redirect_uri
76 | self.token_updater = token_updater
77 |
78 | if token:
79 | self.scope = " ".join(token["scope"])
80 |
81 | else:
82 | self.scope = " ".join(ALL_SCOPES) if not scope else scope
83 |
84 | self.extra = {"client_id": self.client_id, "client_secret": self.client_secret}
85 |
86 | self._oauth = OAuth2Session(
87 | client_id=self.client_id,
88 | token=token,
89 | token_updater=self.token_updater,
90 | redirect_uri=self.redirect_uri,
91 | scope=self.scope,
92 | )
93 |
94 | def refresh_tokens(self) -> Dict[str, Union[str, int]]:
95 | """Refresh and return new tokens."""
96 | token = self._oauth.refresh_token(AUTH_REQ, **self.extra)
97 |
98 | if self.token_updater is not None:
99 | self.token_updater(token)
100 |
101 | return token
102 |
103 | def post_request(
104 | self,
105 | url: str,
106 | params: Optional[Dict] = None,
107 | timeout: int = 5,
108 | ) -> Any:
109 | """Wrapper for post requests."""
110 | resp = None
111 | if not params:
112 | params = {}
113 |
114 | if "json" in params:
115 | json_params: Optional[str] = params.pop("json")
116 |
117 | else:
118 | json_params = None
119 |
120 | if "https://" not in url:
121 | try:
122 | resp = requests.post(url, data=params, timeout=timeout)
123 | except requests.exceptions.ChunkedEncodingError:
124 | LOG.debug("Encoding error when connecting to '%s'", url)
125 | except requests.exceptions.ConnectTimeout:
126 | LOG.debug("Connection to %s timed out", url)
127 | except requests.exceptions.ConnectionError:
128 | LOG.debug("Remote end closed connection without response (%s)", url)
129 |
130 | else:
131 |
132 | def query(url: str, params: Dict, timeout: int, retries: int) -> Any:
133 | if retries == 0:
134 | LOG.error("Too many retries")
135 | return
136 |
137 | try:
138 | if json_params:
139 | rsp = self._oauth.post(
140 | url=url,
141 | json=json_params,
142 | timeout=timeout,
143 | )
144 |
145 | else:
146 | rsp = self._oauth.post(url=url, data=params, timeout=timeout)
147 |
148 | return rsp
149 |
150 | except (
151 | TokenExpiredError,
152 | requests.exceptions.ReadTimeout,
153 | requests.exceptions.ConnectionError,
154 | ):
155 | self._oauth.token = self.refresh_tokens()
156 | # Sleep for 1 sec to prevent authentication related
157 | # timeouts after a token refresh.
158 | sleep(1)
159 | return query(url, params, timeout * 2, retries - 1)
160 |
161 | resp = query(url, params, timeout, 3)
162 |
163 | if resp is None:
164 | LOG.debug("Resp is None - %s", resp)
165 | return None
166 |
167 | if not resp.ok:
168 | LOG.debug("The Netatmo API returned %s", resp.status_code)
169 | LOG.debug("Netato API error: %s", resp.content)
170 | try:
171 | raise ApiError(
172 | f"{resp.status_code} - "
173 | f"{ERRORS.get(resp.status_code, '')} - "
174 | f"{resp.json()['error']['message']} "
175 | f"({resp.json()['error']['code']}) "
176 | f"when accessing '{url}'",
177 | )
178 |
179 | except JSONDecodeError as exc:
180 | raise ApiError(
181 | f"{resp.status_code} - "
182 | f"{ERRORS.get(resp.status_code, '')} - "
183 | f"when accessing '{url}'",
184 | ) from exc
185 |
186 | try:
187 | if "application/json" in resp.headers.get("content-type", []):
188 | return resp.json()
189 |
190 | if resp.content not in [b"", b"None"]:
191 | return resp.content
192 |
193 | except (TypeError, AttributeError):
194 | LOG.debug("Invalid response %s", resp)
195 |
196 | return None
197 |
198 | def get_authorization_url(self, state: Optional[str] = None) -> Tuple[str, str]:
199 | return self._oauth.authorization_url(AUTH_URL, state)
200 |
201 | def request_token(
202 | self,
203 | authorization_response: Optional[str] = None,
204 | code: Optional[str] = None,
205 | ) -> Dict[str, str]:
206 | """
207 | Generic method for fetching a Netatmo access token.
208 | :param authorization_response: Authorization response URL, the callback
209 | URL of the request back to you.
210 | :param code: Authorization code
211 | :return: A token dict
212 | """
213 | return self._oauth.fetch_token(
214 | AUTH_REQ,
215 | authorization_response=authorization_response,
216 | code=code,
217 | client_secret=self.client_secret,
218 | include_client_id=True,
219 | )
220 |
221 | def addwebhook(self, webhook_url: str) -> None:
222 | post_params = {"url": webhook_url}
223 | resp = self.post_request(WEBHOOK_URL_ADD, post_params)
224 | LOG.debug("addwebhook: %s", resp)
225 |
226 | def dropwebhook(self) -> None:
227 | post_params = {"app_types": "app_security"}
228 | resp = self.post_request(WEBHOOK_URL_DROP, post_params)
229 | LOG.debug("dropwebhook: %s", resp)
230 |
231 |
232 | class ClientAuth(NetatmoOAuth2):
233 | """
234 | Request authentication and keep access token available through token method. Renew it automatically if necessary
235 | Args:
236 | clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com
237 | clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com
238 | username (str)
239 | password (str)
240 | scope (Optional[str]):
241 | read_station: to retrieve weather station data (Getstationsdata, Getmeasure)
242 | read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture)
243 | access_camera: to access the camera, the videos and the live stream
244 | write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome)
245 | read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata)
246 | write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint)
247 | read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture)
248 | access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status
249 | read_homecoach: to retrieve Home Coache data (Gethomecoachsdata)
250 | read_smokedetector: to retrieve the smoke detector status (Gethomedata)
251 | Several value can be used at the same time, ie: 'read_station read_camera'
252 | """
253 |
254 | def __init__(
255 | self,
256 | client_id: str,
257 | client_secret: str,
258 | username: str,
259 | password: str,
260 | scope="read_station",
261 | ):
262 | super().__init__(client_id=client_id, client_secret=client_secret, scope=scope)
263 |
264 | self._oauth = OAuth2Session(
265 | client=LegacyApplicationClient(client_id=self.client_id),
266 | )
267 | self._oauth.fetch_token(
268 | token_url=AUTH_REQ,
269 | username=username,
270 | password=password,
271 | client_id=self.client_id,
272 | client_secret=self.client_secret,
273 | scope=self.scope,
274 | )
275 |
--------------------------------------------------------------------------------
/src/pyatmo/exceptions.py:
--------------------------------------------------------------------------------
1 | class NoSchedule(Exception):
2 | pass
3 |
4 |
5 | class InvalidHome(Exception):
6 | pass
7 |
8 |
9 | class InvalidRoom(Exception):
10 | pass
11 |
12 |
13 | class NoDevice(Exception):
14 | pass
15 |
16 |
17 | class ApiError(Exception):
18 | pass
19 |
--------------------------------------------------------------------------------
/src/pyatmo/helpers.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from calendar import timegm
4 | from datetime import datetime
5 | from typing import Dict, Tuple
6 |
7 | LOG: logging.Logger = logging.getLogger(__name__)
8 |
9 | _BASE_URL: str = "https://api.netatmo.com/"
10 |
11 | ERRORS: Dict[int, str] = {
12 | 400: "Bad request",
13 | 401: "Unauthorized",
14 | 403: "Forbidden",
15 | 404: "Not found",
16 | 406: "Not Acceptable",
17 | 500: "Internal Server Error",
18 | 502: "Bad Gateway",
19 | 503: "Service Unavailable",
20 | }
21 |
22 |
23 | def to_time_string(value: str) -> str:
24 | return datetime.utcfromtimestamp(int(value)).isoformat(sep="_")
25 |
26 |
27 | def to_epoch(value: str) -> int:
28 | return timegm(time.strptime(value + "GMT", "%Y-%m-%d_%H:%M:%S%Z"))
29 |
30 |
31 | def today_stamps() -> Tuple[int, int]:
32 | today: int = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z"))
33 | return today, today + 3600 * 24
34 |
35 |
36 | def fix_id(raw_data: Dict) -> Dict:
37 | if raw_data:
38 | for station in raw_data:
39 | station["_id"] = station["_id"].replace(" ", "")
40 | for module in station.get("modules", {}):
41 | module["_id"] = module["_id"].replace(" ", "")
42 | return raw_data
43 |
--------------------------------------------------------------------------------
/src/pyatmo/home_coach.py:
--------------------------------------------------------------------------------
1 | from .auth import NetatmoOAuth2
2 | from .helpers import _BASE_URL
3 | from .weather_station import WeatherStationData
4 |
5 | _GETHOMECOACHDATA_REQ = _BASE_URL + "api/gethomecoachsdata"
6 |
7 |
8 | class HomeCoachData(WeatherStationData):
9 | """
10 | Class of Netatmo Home Couch devices (stations and modules)
11 | """
12 |
13 | def __init__(self, auth: NetatmoOAuth2) -> None:
14 | """Initialize self.
15 |
16 | Arguments:
17 | auth {NetatmoOAuth2} -- Authentication information with a valid access token
18 |
19 | Raises:
20 | NoDevice: No devices found.
21 | """
22 | super().__init__(auth, url_req=_GETHOMECOACHDATA_REQ)
23 |
--------------------------------------------------------------------------------
/src/pyatmo/public_data.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from .auth import NetatmoOAuth2
4 | from .exceptions import NoDevice
5 | from .helpers import _BASE_URL, to_time_string
6 |
7 | _GETPUBLIC_DATA = _BASE_URL + "api/getpublicdata"
8 |
9 | _STATION_TEMPERATURE_TYPE = "temperature"
10 | _STATION_PRESSURE_TYPE = "pressure"
11 | _STATION_HUMIDITY_TYPE = "humidity"
12 |
13 | _ACCESSORY_RAIN_LIVE_TYPE = "rain_live"
14 | _ACCESSORY_RAIN_60MIN_TYPE = "rain_60min"
15 | _ACCESSORY_RAIN_24H_TYPE = "rain_24h"
16 | _ACCESSORY_RAIN_TIME_TYPE = "rain_timeutc"
17 | _ACCESSORY_WIND_STRENGTH_TYPE = "wind_strength"
18 | _ACCESSORY_WIND_ANGLE_TYPE = "wind_angle"
19 | _ACCESSORY_WIND_TIME_TYPE = "wind_timeutc"
20 | _ACCESSORY_GUST_STRENGTH_TYPE = "gust_strength"
21 | _ACCESSORY_GUST_ANGLE_TYPE = "gust_angle"
22 |
23 |
24 | class PublicData:
25 | """
26 | Class of Netatmo public weather data.
27 | """
28 |
29 | def __init__(
30 | self,
31 | auth: NetatmoOAuth2,
32 | lat_ne: str,
33 | lon_ne: str,
34 | lat_sw: str,
35 | lon_sw: str,
36 | required_data_type: str = None,
37 | filtering: bool = False,
38 | ) -> None:
39 | """Initialize self.
40 |
41 | Arguments:
42 | auth {NetatmoOAuth2} -- Authentication information with a valid access token
43 | LAT_NE {str} -- Latitude of the north east corner of the requested area. (-85 <= LAT_NE <= 85 and LAT_NE > LAT_SW)
44 | LON_NE {str} -- Longitude of the north east corner of the requested area. (-180 <= LON_NE <= 180 and LON_NE > LON_SW)
45 | LAT_SW {str} -- latitude of the south west corner of the requested area. (-85 <= LAT_SW <= 85)
46 | LON_SW {str} -- Longitude of the south west corner of the requested area. (-180 <= LON_SW <= 180)
47 |
48 | Keyword Arguments:
49 | required_data_type {str} -- comma-separated list from above _STATION or _ACCESSORY values (default: {None})
50 |
51 | Raises:
52 | NoDevice: No devices found.
53 | """
54 | self.auth = auth
55 | post_params: Dict = {
56 | "lat_ne": lat_ne,
57 | "lon_ne": lon_ne,
58 | "lat_sw": lat_sw,
59 | "lon_sw": lon_sw,
60 | "filter": filtering,
61 | }
62 |
63 | if required_data_type:
64 | post_params["required_data"] = required_data_type
65 |
66 | resp = self.auth.post_request(url=_GETPUBLIC_DATA, params=post_params)
67 | try:
68 | self.raw_data = resp["body"]
69 | except (KeyError, TypeError) as exc:
70 | raise NoDevice("No public weather data returned by Netatmo server") from exc
71 |
72 | self.status = resp["status"]
73 | self.time_exec = to_time_string(resp["time_exec"])
74 | self.time_server = to_time_string(resp["time_server"])
75 |
76 | def stations_in_area(self) -> int:
77 | return len(self.raw_data)
78 |
79 | def get_latest_rain(self) -> Dict:
80 | return self.get_accessory_data(_ACCESSORY_RAIN_LIVE_TYPE)
81 |
82 | def get_average_rain(self) -> float:
83 | return average(self.get_latest_rain())
84 |
85 | def get_60_min_rain(self) -> Dict:
86 | return self.get_accessory_data(_ACCESSORY_RAIN_60MIN_TYPE)
87 |
88 | def get_average_60_min_rain(self) -> float:
89 | return average(self.get_60_min_rain())
90 |
91 | def get_24_h_rain(self) -> Dict:
92 | return self.get_accessory_data(_ACCESSORY_RAIN_24H_TYPE)
93 |
94 | def get_average_24_h_rain(self) -> float:
95 | return average(self.get_24_h_rain())
96 |
97 | def get_latest_pressures(self) -> Dict:
98 | return self.get_latest_station_measures(_STATION_PRESSURE_TYPE)
99 |
100 | def get_average_pressure(self) -> float:
101 | return average(self.get_latest_pressures())
102 |
103 | def get_latest_temperatures(self) -> Dict:
104 | return self.get_latest_station_measures(_STATION_TEMPERATURE_TYPE)
105 |
106 | def get_average_temperature(self) -> float:
107 | return average(self.get_latest_temperatures())
108 |
109 | def get_latest_humidities(self) -> Dict:
110 | return self.get_latest_station_measures(_STATION_HUMIDITY_TYPE)
111 |
112 | def get_average_humidity(self) -> float:
113 | return average(self.get_latest_humidities())
114 |
115 | def get_latest_wind_strengths(self) -> Dict:
116 | return self.get_accessory_data(_ACCESSORY_WIND_STRENGTH_TYPE)
117 |
118 | def get_average_wind_strength(self) -> float:
119 | return average(self.get_latest_wind_strengths())
120 |
121 | def get_latest_wind_angles(self) -> Dict:
122 | return self.get_accessory_data(_ACCESSORY_WIND_ANGLE_TYPE)
123 |
124 | def get_latest_gust_strengths(self) -> Dict:
125 | return self.get_accessory_data(_ACCESSORY_GUST_STRENGTH_TYPE)
126 |
127 | def get_average_gust_strength(self) -> float:
128 | return average(self.get_latest_gust_strengths())
129 |
130 | def get_latest_gust_angles(self):
131 | return self.get_accessory_data(_ACCESSORY_GUST_ANGLE_TYPE)
132 |
133 | def get_locations(self) -> Dict:
134 | locations: Dict = {}
135 | for station in self.raw_data:
136 | locations[station["_id"]] = station["place"]["location"]
137 |
138 | return locations
139 |
140 | def get_time_for_rain_measures(self) -> Dict:
141 | return self.get_accessory_data(_ACCESSORY_RAIN_TIME_TYPE)
142 |
143 | def get_time_for_wind_measures(self) -> Dict:
144 | return self.get_accessory_data(_ACCESSORY_WIND_TIME_TYPE)
145 |
146 | def get_latest_station_measures(self, data_type) -> Dict:
147 | measures: Dict = {}
148 | for station in self.raw_data:
149 | for module in station["measures"].values():
150 | if (
151 | "type" in module
152 | and data_type in module["type"]
153 | and "res" in module
154 | and module["res"]
155 | ):
156 | measure_index = module["type"].index(data_type)
157 | latest_timestamp = sorted(module["res"], reverse=True)[0]
158 | measures[station["_id"]] = module["res"][latest_timestamp][
159 | measure_index
160 | ]
161 |
162 | return measures
163 |
164 | def get_accessory_data(self, data_type: str) -> Dict[str, Any]:
165 | data: Dict = {}
166 | for station in self.raw_data:
167 | for module in station["measures"].values():
168 | if data_type in module:
169 | data[station["_id"]] = module[data_type]
170 |
171 | return data
172 |
173 |
174 | def average(data: dict) -> float:
175 | return sum(data.values()) / len(data) if data else 0.0
176 |
--------------------------------------------------------------------------------
/src/pyatmo/thermostat.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from collections import defaultdict
3 | from typing import Any, Dict, Optional
4 |
5 | from .auth import NetatmoOAuth2
6 | from .exceptions import InvalidRoom, NoDevice, NoSchedule
7 | from .helpers import _BASE_URL
8 |
9 | LOG = logging.getLogger(__name__)
10 |
11 | _GETHOMESDATA_REQ = _BASE_URL + "api/homesdata"
12 | _GETHOMESTATUS_REQ = _BASE_URL + "api/homestatus"
13 | _SETTHERMMODE_REQ = _BASE_URL + "api/setthermmode"
14 | _SETROOMTHERMPOINT_REQ = _BASE_URL + "api/setroomthermpoint"
15 | _GETROOMMEASURE_REQ = _BASE_URL + "api/getroommeasure"
16 | _SWITCHHOMESCHEDULE_REQ = _BASE_URL + "api/switchhomeschedule"
17 |
18 |
19 | class HomeData:
20 | """
21 | Class of Netatmo energy devices (relays, thermostat modules and valves)
22 | """
23 |
24 | def __init__(self, auth: NetatmoOAuth2) -> None:
25 | """Initialize self.
26 |
27 | Arguments:
28 | auth {NetatmoOAuth2} -- Authentication information with a valid access token
29 |
30 | Raises:
31 | NoDevice: No devices found.
32 | """
33 | self.auth = auth
34 | resp = self.auth.post_request(url=_GETHOMESDATA_REQ)
35 | if resp is None or "body" not in resp:
36 | raise NoDevice("No thermostat data returned by Netatmo server")
37 |
38 | self.raw_data = resp["body"].get("homes")
39 | if not self.raw_data:
40 | raise NoDevice("No thermostat data available")
41 |
42 | self.homes: Dict = {d["id"]: d for d in self.raw_data}
43 |
44 | self.modules: Dict = defaultdict(dict)
45 | self.rooms: Dict = defaultdict(dict)
46 | self.schedules: Dict = defaultdict(dict)
47 | self.zones: Dict = defaultdict(dict)
48 | self.setpoint_duration: Dict = defaultdict(dict)
49 |
50 | for item in self.raw_data:
51 | home_id = item.get("id")
52 | home_name = item.get("name")
53 |
54 | if not home_name:
55 | home_name = "Unknown"
56 | self.homes[home_id]["name"] = home_name
57 |
58 | if "modules" not in item:
59 | continue
60 |
61 | for module in item["modules"]:
62 | self.modules[home_id][module["id"]] = module
63 |
64 | self.setpoint_duration[home_id] = item.get(
65 | "therm_setpoint_default_duration",
66 | )
67 |
68 | for room in item.get("rooms", []):
69 | self.rooms[home_id][room["id"]] = room
70 |
71 | for schedule in item.get("therm_schedules", []):
72 | schedule_id = schedule["id"]
73 | self.schedules[home_id][schedule_id] = schedule
74 |
75 | if schedule_id not in self.zones[home_id]:
76 | self.zones[home_id][schedule_id] = {}
77 |
78 | for zone in schedule["zones"]:
79 | self.zones[home_id][schedule_id][zone["id"]] = zone
80 |
81 | def _get_selected_schedule(self, home_id: str) -> Dict:
82 | """Get the selected schedule for a given home ID."""
83 | for value in self.schedules.get(home_id, {}).values():
84 | if "selected" in value.keys():
85 | return value
86 |
87 | return {}
88 |
89 | def switch_home_schedule(self, home_id: str, schedule_id: str) -> Any:
90 | """Switch the schedule for a give home ID."""
91 | schedules = {
92 | self.schedules[home_id][s]["name"]: self.schedules[home_id][s]["id"]
93 | for s in self.schedules.get(home_id, {})
94 | }
95 | if schedule_id not in list(schedules.values()):
96 | raise NoSchedule("%s is not a valid schedule id" % schedule_id)
97 |
98 | post_params = {
99 | "home_id": home_id,
100 | "schedule_id": schedule_id,
101 | }
102 | resp = self.auth.post_request(url=_SWITCHHOMESCHEDULE_REQ, params=post_params)
103 | LOG.debug("Response: %s", resp)
104 |
105 | def get_hg_temp(self, home_id: str) -> Optional[float]:
106 | """Return frost guard temperature value."""
107 | return self._get_selected_schedule(home_id).get("hg_temp")
108 |
109 | def get_away_temp(self, home_id: str) -> Optional[float]:
110 | """Return the configured away temperature value."""
111 | return self._get_selected_schedule(home_id).get("away_temp")
112 |
113 | def get_thermostat_type(self, home_id: str, room_id: str) -> Optional[str]:
114 | """Return the thermostat type of the room."""
115 | for module in self.modules.get(home_id, {}).values():
116 | if module.get("room_id") == room_id:
117 | return module.get("type")
118 |
119 | return None
120 |
121 |
122 | class HomeStatus:
123 | def __init__(self, auth: NetatmoOAuth2, home_id: str):
124 | self.auth = auth
125 |
126 | self.home_id = home_id
127 | post_params = {"home_id": self.home_id}
128 |
129 | resp = self.auth.post_request(url=_GETHOMESTATUS_REQ, params=post_params)
130 | if (
131 | "errors" in resp
132 | or "body" not in resp
133 | or "home" not in resp["body"]
134 | or ("errors" in resp["body"] and "modules" not in resp["body"]["home"])
135 | ):
136 | LOG.error("Errors in response: %s", resp)
137 | raise NoDevice("No device found, errors in response")
138 |
139 | self.raw_data = resp["body"]["home"]
140 | self.rooms: Dict = {}
141 | self.thermostats: Dict = defaultdict(dict)
142 | self.valves: Dict = defaultdict(dict)
143 | self.relays: Dict = defaultdict(dict)
144 |
145 | for room in self.raw_data.get("rooms", []):
146 | self.rooms[room["id"]] = room
147 |
148 | for module in self.raw_data.get("modules", []):
149 | if module["type"] == "NATherm1":
150 | self.thermostats[module["id"]] = module
151 |
152 | elif module["type"] == "NRV":
153 | self.valves[module["id"]] = module
154 |
155 | elif module["type"] == "NAPlug":
156 | self.relays[module["id"]] = module
157 |
158 | def get_room(self, room_id: str) -> Dict:
159 | for key, value in self.rooms.items():
160 | if value["id"] == room_id:
161 | return self.rooms[key]
162 |
163 | raise InvalidRoom("No room with ID %s" % room_id)
164 |
165 | def get_thermostat(self, room_id: str) -> Dict:
166 | """Return thermostat data for a given room id."""
167 | for key, value in self.thermostats.items():
168 | if value["id"] == room_id:
169 | return self.thermostats[key]
170 |
171 | raise InvalidRoom("No room with ID %s" % room_id)
172 |
173 | def get_relay(self, room_id: str) -> Dict:
174 | for key, value in self.relays.items():
175 | if value["id"] == room_id:
176 | return self.relays[key]
177 |
178 | raise InvalidRoom("No room with ID %s" % room_id)
179 |
180 | def get_valve(self, room_id: str) -> Dict:
181 | for key, value in self.valves.items():
182 | if value["id"] == room_id:
183 | return self.valves[key]
184 |
185 | raise InvalidRoom("No room with ID %s" % room_id)
186 |
187 | def set_point(self, room_id: str) -> Optional[float]:
188 | """Return the setpoint of a given room."""
189 | return self.get_room(room_id).get("therm_setpoint_temperature")
190 |
191 | def set_point_mode(self, room_id: str) -> Optional[str]:
192 | """Return the setpointmode of a given room."""
193 | return self.get_room(room_id).get("therm_setpoint_mode")
194 |
195 | def measured_temperature(self, room_id: str) -> Optional[float]:
196 | """Return the measured temperature of a given room."""
197 | return self.get_room(room_id).get("therm_measured_temperature")
198 |
199 | def boiler_status(self, module_id: str) -> Optional[bool]:
200 | return self.get_thermostat(module_id).get("boiler_status")
201 |
202 | def set_thermmode(
203 | self,
204 | mode: str,
205 | end_time: int = None,
206 | schedule_id: str = None,
207 | ) -> Optional[str]:
208 | post_params = {
209 | "home_id": self.home_id,
210 | "mode": mode,
211 | }
212 | if end_time is not None and mode in ("hg", "away"):
213 | post_params["endtime"] = str(end_time)
214 |
215 | if schedule_id is not None and mode == "schedule":
216 | post_params["schedule_id"] = schedule_id
217 |
218 | return self.auth.post_request(url=_SETTHERMMODE_REQ, params=post_params)
219 |
220 | def set_room_thermpoint(
221 | self,
222 | room_id: str,
223 | mode: str,
224 | temp: float = None,
225 | end_time: int = None,
226 | ) -> Optional[str]:
227 | post_params = {
228 | "home_id": self.home_id,
229 | "room_id": room_id,
230 | "mode": mode,
231 | }
232 | # Temp and endtime should only be send when mode=='manual', but netatmo api can
233 | # handle that even when mode == 'home' and these settings don't make sense
234 | if temp is not None:
235 | post_params["temp"] = str(temp)
236 |
237 | if end_time is not None:
238 | post_params["endtime"] = str(end_time)
239 |
240 | return self.auth.post_request(url=_SETROOMTHERMPOINT_REQ, params=post_params)
241 |
--------------------------------------------------------------------------------
/src/pyatmo/weather_station.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from typing import Dict, List, Optional, Tuple
4 |
5 | from .auth import NetatmoOAuth2
6 | from .exceptions import NoDevice
7 | from .helpers import _BASE_URL, fix_id, today_stamps
8 |
9 | LOG = logging.getLogger(__name__)
10 |
11 | _GETMEASURE_REQ = _BASE_URL + "api/getmeasure"
12 | _GETSTATIONDATA_REQ = _BASE_URL + "api/getstationsdata"
13 |
14 |
15 | class WeatherStationData:
16 | """Class of Netatmo Weather Station devices (stations and modules)."""
17 |
18 | def __init__(self, auth: NetatmoOAuth2, url_req: str = None) -> None:
19 | """Initialize self.
20 |
21 | Arguments:
22 | auth {NetatmoOAuth2} -- Authentication information with a valid access token
23 |
24 | Raises:
25 | NoDevice: No devices found.
26 | """
27 | self.url_req = url_req or _GETSTATIONDATA_REQ
28 | self.auth = auth
29 |
30 | resp = self.auth.post_request(url=self.url_req)
31 |
32 | if resp is None or "body" not in resp:
33 | raise NoDevice("No weather station data returned by Netatmo server")
34 |
35 | try:
36 | self.raw_data = fix_id(resp["body"].get("devices"))
37 | except KeyError as exc:
38 | LOG.debug("No in response %s", resp)
39 | raise NoDevice(
40 | "No weather station data returned by Netatmo server",
41 | ) from exc
42 |
43 | if not self.raw_data:
44 | raise NoDevice("No weather station available")
45 |
46 | self.stations = {d["_id"]: d for d in self.raw_data}
47 | self.modules = {}
48 |
49 | for item in self.raw_data:
50 | # The station name is sometimes not contained in the backend data
51 | if "station_name" not in item:
52 | item["station_name"] = item.get("home_name", item["type"])
53 |
54 | if "modules" not in item:
55 | item["modules"] = [item]
56 |
57 | for module in item["modules"]:
58 | if "module_name" not in module and module["type"] == "NHC":
59 | module["module_name"] = module["station_name"]
60 |
61 | self.modules[module["_id"]] = module
62 | self.modules[module["_id"]]["main_device"] = item["_id"]
63 |
64 | def get_module_names(self, station_id: str) -> List:
65 | """Return a list of all module names for a given station."""
66 | res = set()
67 | station_data = self.get_station(station_id)
68 |
69 | if not station_data:
70 | return []
71 |
72 | res.add(station_data.get("module_name", station_data.get("type")))
73 | for module in station_data["modules"]:
74 | # Add module name, use module type if no name is available
75 | res.add(module.get("module_name", module.get("type")))
76 |
77 | return list(res)
78 |
79 | def get_modules(self, station_id: str) -> Dict:
80 | """Return a dict of modules per given station."""
81 | station_data = self.get_station(station_id)
82 |
83 | if not station_data:
84 | return {}
85 |
86 | res = {}
87 | for station in [self.stations[station_data["_id"]]]:
88 | station_type = station.get("type")
89 | station_name = station.get("station_name", station_type)
90 | res[station["_id"]] = {
91 | "station_name": station_name,
92 | "module_name": station.get("module_name", station_type),
93 | "id": station["_id"],
94 | }
95 |
96 | for module in station["modules"]:
97 | res[module["_id"]] = {
98 | "station_name": module.get("station_name", station_name),
99 | "module_name": module.get("module_name", module.get("type")),
100 | "id": module["_id"],
101 | }
102 |
103 | return res
104 |
105 | def get_station(self, station_id: str) -> Dict:
106 | """Return station by id."""
107 | return self.stations.get(station_id, {})
108 |
109 | def get_module(self, module_id: str) -> Dict:
110 | """Return module by id."""
111 | return self.modules.get(module_id, {})
112 |
113 | def get_monitored_conditions(self, module_id: str) -> List:
114 | """Return monitored conditions for given module."""
115 | module = self.get_module(module_id)
116 | if not module:
117 | module = self.get_station(module_id)
118 |
119 | if not module:
120 | return []
121 |
122 | conditions = []
123 | for condition in module.get("data_type", []):
124 | if condition == "Wind":
125 | # the Wind meter actually exposes the following conditions
126 | conditions.extend(
127 | ["WindAngle", "WindStrength", "GustAngle", "GustStrength"],
128 | )
129 |
130 | elif condition == "Rain":
131 | conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"])
132 |
133 | else:
134 | conditions.append(condition)
135 |
136 | if module["type"] in ["NAMain", "NHC"]:
137 | # the main module has wifi_status
138 | conditions.append("wifi_status")
139 |
140 | else:
141 | # assume all other modules have rf_status, battery_vp, and battery_percent
142 | conditions.extend(["rf_status", "battery_vp", "battery_percent"])
143 |
144 | if module["type"] in ["NAMain", "NAModule1", "NAModule4"]:
145 | conditions.extend(["temp_trend"])
146 |
147 | if module["type"] == "NAMain":
148 | conditions.extend(["pressure_trend"])
149 |
150 | if module["type"] in [
151 | "NAMain",
152 | "NAModule1",
153 | "NAModule2",
154 | "NAModule3",
155 | "NAModule4",
156 | "NHC",
157 | ]:
158 | conditions.append("reachable")
159 |
160 | return conditions
161 |
162 | def get_last_data(self, station_id: str, exclude: int = 0) -> Dict:
163 | """Return data for a given station and time frame."""
164 | key = "_id"
165 |
166 | # Breaking change from Netatmo : dashboard_data no longer available if station lost
167 | last_data: Dict = {}
168 | station = self.get_station(station_id)
169 |
170 | if not station or "dashboard_data" not in station:
171 | LOG.debug("No dashboard data for station %s", station_id)
172 | return last_data
173 |
174 | # Define oldest acceptable sensor measure event
175 | limit = (time.time() - exclude) if exclude else 0
176 |
177 | data = station["dashboard_data"]
178 | if key in station and data["time_utc"] > limit:
179 | last_data[station[key]] = data.copy()
180 | last_data[station[key]]["When"] = last_data[station[key]].pop("time_utc")
181 | last_data[station[key]]["wifi_status"] = station.get("wifi_status")
182 | last_data[station[key]]["reachable"] = station.get("reachable")
183 |
184 | for module in station["modules"]:
185 |
186 | if "dashboard_data" not in module or key not in module:
187 | continue
188 |
189 | data = module["dashboard_data"]
190 | if "time_utc" in data and data["time_utc"] > limit:
191 | last_data[module[key]] = data.copy()
192 | last_data[module[key]]["When"] = last_data[module[key]].pop("time_utc")
193 |
194 | # For potential use, add battery and radio coverage information to module data if present
195 | for i in (
196 | "rf_status",
197 | "battery_vp",
198 | "battery_percent",
199 | "reachable",
200 | "wifi_status",
201 | ):
202 | if i in module:
203 | last_data[module[key]][i] = module[i]
204 |
205 | return last_data
206 |
207 | def check_not_updated(self, station_id: str, delay: int = 3600) -> List:
208 | """Check if a given station has not been updated."""
209 | res = self.get_last_data(station_id)
210 | return [
211 | key for key, value in res.items() if time.time() - value["When"] > delay
212 | ]
213 |
214 | def check_updated(self, station_id: str, delay: int = 3600) -> List:
215 | """Check if a given station has been updated."""
216 | res = self.get_last_data(station_id)
217 | return [
218 | key for key, value in res.items() if time.time() - value["When"] < delay
219 | ]
220 |
221 | def get_data(
222 | self,
223 | device_id: str,
224 | scale: str,
225 | module_type: str,
226 | module_id: str = None,
227 | date_begin: float = None,
228 | date_end: float = None,
229 | limit: int = None,
230 | optimize: bool = False,
231 | real_time: bool = False,
232 | ) -> Optional[Dict]:
233 | """Retrieve data from a device or module."""
234 | post_params = {"device_id": device_id}
235 | if module_id:
236 | post_params["module_id"] = module_id
237 |
238 | post_params["scale"] = scale
239 | post_params["type"] = module_type
240 |
241 | if date_begin:
242 | post_params["date_begin"] = f"{date_begin}"
243 |
244 | if date_end:
245 | post_params["date_end"] = f"{date_end}"
246 |
247 | if limit:
248 | post_params["limit"] = f"{limit}"
249 |
250 | post_params["optimize"] = "true" if optimize else "false"
251 | post_params["real_time"] = "true" if real_time else "false"
252 |
253 | return self.auth.post_request(url=_GETMEASURE_REQ, params=post_params)
254 |
255 | def get_min_max_t_h(
256 | self,
257 | station_id: str,
258 | module_id: str = None,
259 | frame: str = "last24",
260 | ) -> Optional[Tuple[float, float, float, float]]:
261 | """Return minimum and maximum temperature and humidity over the given timeframe.
262 |
263 | Arguments:
264 | station_id {str} -- Station ID
265 |
266 | Keyword Arguments:
267 | module_id {str} -- Module ID (default: {None})
268 | frame {str} -- Timeframe can be "last24" or "day" (default: {"last24"})
269 |
270 | Returns:
271 | (min_t {float}, max_t {float}, min_h {float}, max_h {float}) -- minimum and maximum for temperature and humidity
272 | """
273 | if frame == "last24":
274 | end = time.time()
275 | start = end - 24 * 3600 # 24 hours ago
276 |
277 | elif frame == "day":
278 | start, end = today_stamps()
279 |
280 | else:
281 | raise ValueError("'frame' value can only be 'last24' or 'day'")
282 |
283 | resp = self.get_data(
284 | device_id=station_id,
285 | module_id=module_id,
286 | scale="max",
287 | module_type="Temperature,Humidity",
288 | date_begin=start,
289 | date_end=end,
290 | )
291 |
292 | if resp:
293 | temperature = [temp[0] for temp in resp["body"].values()]
294 | humidity = [hum[1] for hum in resp["body"].values()]
295 | return min(temperature), max(temperature), min(humidity), max(humidity)
296 |
297 | return None
298 |
--------------------------------------------------------------------------------
/src/version.py:
--------------------------------------------------------------------------------
1 | MAJOR_VERSION = 4
2 | MINOR_VERSION = 2
3 | PATCH_VERSION = 1
4 | __version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
5 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jabesq/netatmo-api-python/42848d146b9fee922790c10c24ee3fd94aab1ac2/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Define shared fixtures."""
2 | # pylint: disable=redefined-outer-name, protected-access
3 | import json
4 | from contextlib import contextmanager
5 |
6 | import pytest
7 |
8 | import pyatmo
9 |
10 |
11 | @contextmanager
12 | def does_not_raise():
13 | yield
14 |
15 |
16 | @pytest.fixture(scope="function")
17 | def auth(requests_mock):
18 | with open("fixtures/oauth2_token.json") as json_file:
19 | json_fixture = json.load(json_file)
20 | requests_mock.post(
21 | pyatmo.auth.AUTH_REQ,
22 | json=json_fixture,
23 | headers={"content-type": "application/json"},
24 | )
25 | return pyatmo.ClientAuth(
26 | client_id="CLIENT_ID",
27 | client_secret="CLIENT_SECRET",
28 | username="USERNAME",
29 | password="PASSWORD",
30 | scope=" ".join(pyatmo.auth.ALL_SCOPES),
31 | )
32 |
33 |
34 | @pytest.fixture(scope="function")
35 | def home_data(auth, requests_mock):
36 | with open("fixtures/home_data_simple.json") as json_file:
37 | json_fixture = json.load(json_file)
38 | requests_mock.post(
39 | pyatmo.thermostat._GETHOMESDATA_REQ,
40 | json=json_fixture,
41 | headers={"content-type": "application/json"},
42 | )
43 | return pyatmo.HomeData(auth)
44 |
45 |
46 | @pytest.fixture(scope="function")
47 | def home_status(auth, home_id, requests_mock):
48 | with open("fixtures/home_status_simple.json") as json_file:
49 | json_fixture = json.load(json_file)
50 | requests_mock.post(
51 | pyatmo.thermostat._GETHOMESTATUS_REQ,
52 | json=json_fixture,
53 | headers={"content-type": "application/json"},
54 | )
55 | return pyatmo.HomeStatus(auth, home_id)
56 |
57 |
58 | @pytest.fixture(scope="function")
59 | def public_data(auth, requests_mock):
60 | with open("fixtures/public_data_simple.json") as json_file:
61 | json_fixture = json.load(json_file)
62 | requests_mock.post(
63 | pyatmo.public_data._GETPUBLIC_DATA,
64 | json=json_fixture,
65 | headers={"content-type": "application/json"},
66 | )
67 |
68 | lon_ne = 6.221652
69 | lat_ne = 46.610870
70 | lon_sw = 6.217828
71 | lat_sw = 46.596485
72 |
73 | return pyatmo.PublicData(auth, lat_ne, lon_ne, lat_sw, lon_sw)
74 |
75 |
76 | @pytest.fixture(scope="function")
77 | def weather_station_data(auth, requests_mock):
78 | with open("fixtures/weatherstation_data_simple.json") as json_file:
79 | json_fixture = json.load(json_file)
80 | requests_mock.post(
81 | pyatmo.weather_station._GETSTATIONDATA_REQ,
82 | json=json_fixture,
83 | headers={"content-type": "application/json"},
84 | )
85 | return pyatmo.WeatherStationData(auth)
86 |
87 |
88 | @pytest.fixture(scope="function")
89 | def home_coach_data(auth, requests_mock):
90 | with open("fixtures/home_coach_simple.json") as json_file:
91 | json_fixture = json.load(json_file)
92 | requests_mock.post(
93 | pyatmo.home_coach._GETHOMECOACHDATA_REQ,
94 | json=json_fixture,
95 | headers={"content-type": "application/json"},
96 | )
97 | return pyatmo.HomeCoachData(auth)
98 |
99 |
100 | @pytest.fixture(scope="function")
101 | def camera_home_data(auth, requests_mock):
102 | with open("fixtures/camera_home_data.json") as json_file:
103 | json_fixture = json.load(json_file)
104 | requests_mock.post(
105 | pyatmo.camera._GETHOMEDATA_REQ,
106 | json=json_fixture,
107 | headers={"content-type": "application/json"},
108 | )
109 | for index in ["w", "z", "g"]:
110 | vpn_url = (
111 | f"https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/"
112 | f"6d278460699e56180d47ab47169efb31/"
113 | f"MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTT{index},,"
114 | )
115 | with open("fixtures/camera_ping.json") as json_file:
116 | json_fixture = json.load(json_file)
117 | requests_mock.post(
118 | vpn_url + "/command/ping",
119 | json=json_fixture,
120 | headers={"content-type": "application/json"},
121 | )
122 | local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d"
123 | with open("fixtures/camera_ping.json") as json_file:
124 | json_fixture = json.load(json_file)
125 | requests_mock.post(
126 | local_url + "/command/ping",
127 | json=json_fixture,
128 | headers={"content-type": "application/json"},
129 | )
130 | return pyatmo.CameraData(auth)
131 |
--------------------------------------------------------------------------------
/tests/test_pyatmo.py:
--------------------------------------------------------------------------------
1 | """Define tests for untility methods."""
2 | # pylint: disable=protected-access
3 | import json
4 | import time
5 |
6 | import oauthlib
7 | import pytest
8 |
9 | import pyatmo
10 |
11 |
12 | def test_client_auth(auth):
13 | assert auth._oauth.token["access_token"] == (
14 | "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12"
15 | )
16 | assert auth._oauth.token["refresh_token"] == (
17 | "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93"
18 | )
19 |
20 |
21 | def test_client_auth_invalid(requests_mock):
22 | with open("fixtures/invalid_grant.json") as json_file:
23 | json_fixture = json.load(json_file)
24 | requests_mock.post(
25 | pyatmo.auth.AUTH_REQ,
26 | json=json_fixture,
27 | headers={"content-type": "application/json"},
28 | )
29 | with pytest.raises(oauthlib.oauth2.rfc6749.errors.InvalidGrantError):
30 | pyatmo.ClientAuth(
31 | client_id="CLIENT_ID",
32 | client_secret="CLIENT_SECRET",
33 | username="USERNAME",
34 | password="PASSWORD",
35 | )
36 |
37 |
38 | def test_post_request_json(auth, requests_mock):
39 | """Test wrapper for posting requests against the Netatmo API."""
40 | requests_mock.post(
41 | pyatmo.helpers._BASE_URL,
42 | json={"a": "b"},
43 | headers={"content-type": "application/json"},
44 | )
45 | resp = auth.post_request(pyatmo.helpers._BASE_URL, None)
46 | assert resp == {"a": "b"}
47 |
48 |
49 | def test_post_request_binary(auth, requests_mock):
50 | """Test wrapper for posting requests against the Netatmo API."""
51 | requests_mock.post(
52 | pyatmo.helpers._BASE_URL,
53 | text="Success",
54 | headers={"content-type": "application/text"},
55 | )
56 | resp = auth.post_request(pyatmo.helpers._BASE_URL, None)
57 | assert resp == b"Success"
58 |
59 |
60 | @pytest.mark.parametrize(
61 | "test_input,expected",
62 | [(200, None), (404, None), (401, None)],
63 | )
64 | def test_post_request_fail(auth, requests_mock, test_input, expected):
65 | """Test failing requests against the Netatmo API."""
66 | requests_mock.post(pyatmo.helpers._BASE_URL, status_code=test_input)
67 |
68 | if test_input == 200:
69 | resp = auth.post_request(pyatmo.helpers._BASE_URL, None)
70 | assert resp is expected
71 | else:
72 | with pytest.raises(pyatmo.ApiError):
73 | resp = auth.post_request(pyatmo.helpers._BASE_URL, None)
74 |
75 |
76 | @pytest.mark.parametrize(
77 | "test_input,expected",
78 | [
79 | (1, "1970-01-01_00:00:01"),
80 | (0, "1970-01-01_00:00:00"),
81 | (-1, "1969-12-31_23:59:59"),
82 | (2000000000, "2033-05-18_03:33:20"),
83 | ("1", "1970-01-01_00:00:01"),
84 | pytest.param("A", None, marks=pytest.mark.xfail),
85 | pytest.param([1], None, marks=pytest.mark.xfail),
86 | pytest.param({1}, None, marks=pytest.mark.xfail),
87 | ],
88 | )
89 | def test_to_time_string(test_input, expected):
90 | """Test time to string conversion."""
91 | assert pyatmo.helpers.to_time_string(test_input) == expected
92 |
93 |
94 | @pytest.mark.parametrize(
95 | "test_input,expected",
96 | [
97 | ("1970-01-01_00:00:01", 1),
98 | ("1970-01-01_00:00:00", 0),
99 | ("1969-12-31_23:59:59", -1),
100 | ("2033-05-18_03:33:20", 2000000000),
101 | ],
102 | )
103 | def test_to_epoch(test_input, expected):
104 | """Test time to epoch conversion."""
105 | assert pyatmo.helpers.to_epoch(test_input) == expected
106 |
107 |
108 | @pytest.mark.parametrize(
109 | "test_input,expected",
110 | [
111 | ("2018-06-21", (1529539200, 1529625600)),
112 | ("2000-01-01", (946684800, 946771200)),
113 | pytest.param("2000-04-31", None, marks=pytest.mark.xfail),
114 | ],
115 | )
116 | def test_today_stamps(monkeypatch, test_input, expected):
117 | """Test today_stamps function."""
118 |
119 | def mockreturn(_):
120 | return test_input
121 |
122 | monkeypatch.setattr(time, "strftime", mockreturn)
123 | assert pyatmo.helpers.today_stamps() == expected
124 |
--------------------------------------------------------------------------------
/tests/test_pyatmo_homecoach.py:
--------------------------------------------------------------------------------
1 | """Define tests for HomeCoach module."""
2 | # pylint: disable=protected-access
3 | import json
4 |
5 | import pytest
6 |
7 | import pyatmo
8 |
9 |
10 | def test_home_coach_data(home_coach_data):
11 | assert home_coach_data.stations["12:34:56:26:69:0c"]["station_name"] == "Bedroom"
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "station_id, expected",
16 | [
17 | ("12:34:56:26:69:0c", ["Bedroom"]),
18 | pytest.param(
19 | "NoValidStation",
20 | None,
21 | marks=pytest.mark.xfail(
22 | reason="Invalid station names are not handled yet.",
23 | ),
24 | ),
25 | ],
26 | )
27 | def test_home_coach_data_get_module_names(home_coach_data, station_id, expected):
28 | assert sorted(home_coach_data.get_module_names(station_id)) == expected
29 |
30 |
31 | @pytest.mark.parametrize(
32 | "station_id, expected",
33 | [
34 | (None, {}),
35 | (
36 | "12:34:56:26:69:0c",
37 | {
38 | "12:34:56:26:69:0c": {
39 | "station_name": "Bedroom",
40 | "module_name": "Bedroom",
41 | "id": "12:34:56:26:69:0c",
42 | },
43 | },
44 | ),
45 | pytest.param(
46 | "NoValidStation",
47 | None,
48 | marks=pytest.mark.xfail(
49 | reason="Invalid station names are not handled yet.",
50 | ),
51 | ),
52 | ],
53 | )
54 | def test_home_coach_data_get_modules(home_coach_data, station_id, expected):
55 | assert home_coach_data.get_modules(station_id) == expected
56 |
57 |
58 | def test_home_coach_data_no_devices(auth, requests_mock):
59 | with open("fixtures/home_coach_no_devices.json") as json_file:
60 | json_fixture = json.load(json_file)
61 | requests_mock.post(
62 | pyatmo.home_coach._GETHOMECOACHDATA_REQ,
63 | json=json_fixture,
64 | headers={"content-type": "application/json"},
65 | )
66 | with pytest.raises(pyatmo.NoDevice):
67 | assert pyatmo.home_coach.HomeCoachData(auth)
68 |
--------------------------------------------------------------------------------
/tests/test_pyatmo_publicdata.py:
--------------------------------------------------------------------------------
1 | """Define tests for Public weather module."""
2 | # pylint: disable=protected-access
3 | import json
4 |
5 | import pytest
6 |
7 | import pyatmo
8 |
9 | LON_NE = 6.221652
10 | LAT_NE = 46.610870
11 | LON_SW = 6.217828
12 | LAT_SW = 46.596485
13 |
14 |
15 | def test_public_data(auth, requests_mock):
16 | with open("fixtures/public_data_simple.json") as json_file:
17 | json_fixture = json.load(json_file)
18 | requests_mock.post(
19 | pyatmo.public_data._GETPUBLIC_DATA,
20 | json=json_fixture,
21 | headers={"content-type": "application/json"},
22 | )
23 |
24 | public_data = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW)
25 | assert public_data.status == "ok"
26 |
27 | public_data = pyatmo.PublicData(
28 | auth,
29 | LAT_NE,
30 | LON_NE,
31 | LAT_SW,
32 | LON_SW,
33 | required_data_type="temperature,rain_live",
34 | )
35 | assert public_data.status == "ok"
36 |
37 |
38 | def test_public_data_unavailable(auth, requests_mock):
39 | requests_mock.post(pyatmo.public_data._GETPUBLIC_DATA, status_code=404)
40 | with pytest.raises(pyatmo.ApiError):
41 | pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW)
42 |
43 |
44 | def test_public_data_error(auth, requests_mock):
45 | with open("fixtures/public_data_error_mongo.json") as json_file:
46 | json_fixture = json.load(json_file)
47 | requests_mock.post(
48 | pyatmo.public_data._GETPUBLIC_DATA,
49 | json=json_fixture,
50 | headers={"content-type": "application/json"},
51 | )
52 | with pytest.raises(pyatmo.NoDevice):
53 | pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW)
54 |
55 |
56 | def test_public_data_stations_in_area(public_data):
57 | assert public_data.stations_in_area() == 8
58 |
59 |
60 | def test_public_data_get_latest_rain(public_data):
61 | expected = {
62 | "70:ee:50:1f:68:9e": 0,
63 | "70:ee:50:27:25:b0": 0,
64 | "70:ee:50:36:94:7c": 0.5,
65 | "70:ee:50:36:a9:fc": 0,
66 | }
67 | assert public_data.get_latest_rain() == expected
68 |
69 |
70 | def test_public_data_get_average_rain(public_data):
71 | assert public_data.get_average_rain() == 0.125
72 |
73 |
74 | def test_public_data_get_60_min_rain(public_data):
75 | expected = {
76 | "70:ee:50:1f:68:9e": 0,
77 | "70:ee:50:27:25:b0": 0,
78 | "70:ee:50:36:94:7c": 0.2,
79 | "70:ee:50:36:a9:fc": 0,
80 | }
81 | assert public_data.get_60_min_rain() == expected
82 |
83 |
84 | def test_public_data_get_average_60_min_rain(public_data):
85 | assert public_data.get_average_60_min_rain() == 0.05
86 |
87 |
88 | def test_public_data_get_24_h_rain(public_data):
89 | expected = {
90 | "70:ee:50:1f:68:9e": 9.999,
91 | "70:ee:50:27:25:b0": 11.716000000000001,
92 | "70:ee:50:36:94:7c": 12.322000000000001,
93 | "70:ee:50:36:a9:fc": 11.009,
94 | }
95 | assert public_data.get_24_h_rain() == expected
96 |
97 |
98 | def test_public_data_get_average_24_h_rain(public_data):
99 | assert public_data.get_average_24_h_rain() == 11.261500000000002
100 |
101 |
102 | def test_public_data_get_latest_pressures(public_data):
103 | expected = {
104 | "70:ee:50:1f:68:9e": 1007.3,
105 | "70:ee:50:27:25:b0": 1012.8,
106 | "70:ee:50:36:94:7c": 1010.6,
107 | "70:ee:50:36:a9:fc": 1010,
108 | "70:ee:50:01:20:fa": 1014.4,
109 | "70:ee:50:04:ed:7a": 1005.4,
110 | "70:ee:50:27:9f:2c": 1010.6,
111 | "70:ee:50:3c:02:78": 1011.7,
112 | }
113 | assert public_data.get_latest_pressures() == expected
114 |
115 |
116 | def test_public_data_get_average_pressure(public_data):
117 | assert public_data.get_average_pressure() == 1010.3499999999999
118 |
119 |
120 | def test_public_data_get_latest_temperatures(public_data):
121 | expected = {
122 | "70:ee:50:1f:68:9e": 21.1,
123 | "70:ee:50:27:25:b0": 23.2,
124 | "70:ee:50:36:94:7c": 21.4,
125 | "70:ee:50:36:a9:fc": 20.1,
126 | "70:ee:50:01:20:fa": 27.4,
127 | "70:ee:50:04:ed:7a": 19.8,
128 | "70:ee:50:27:9f:2c": 25.5,
129 | "70:ee:50:3c:02:78": 23.3,
130 | }
131 | assert public_data.get_latest_temperatures() == expected
132 |
133 |
134 | def test_public_data_get_average_temperature(public_data):
135 | assert public_data.get_average_temperature() == 22.725
136 |
137 |
138 | def test_public_data_get_latest_humidities(public_data):
139 | expected = {
140 | "70:ee:50:1f:68:9e": 69,
141 | "70:ee:50:27:25:b0": 60,
142 | "70:ee:50:36:94:7c": 62,
143 | "70:ee:50:36:a9:fc": 67,
144 | "70:ee:50:01:20:fa": 58,
145 | "70:ee:50:04:ed:7a": 76,
146 | "70:ee:50:27:9f:2c": 56,
147 | "70:ee:50:3c:02:78": 58,
148 | }
149 | assert public_data.get_latest_humidities() == expected
150 |
151 |
152 | def test_public_data_get_average_humidity(public_data):
153 | assert public_data.get_average_humidity() == 63.25
154 |
155 |
156 | def test_public_data_get_latest_wind_strengths(public_data):
157 | expected = {"70:ee:50:36:a9:fc": 15}
158 | assert public_data.get_latest_wind_strengths() == expected
159 |
160 |
161 | def test_public_data_get_average_wind_strength(public_data):
162 | assert public_data.get_average_wind_strength() == 15
163 |
164 |
165 | def test_public_data_get_latest_wind_angles(public_data):
166 | expected = {"70:ee:50:36:a9:fc": 17}
167 | assert public_data.get_latest_wind_angles() == expected
168 |
169 |
170 | def test_public_data_get_latest_gust_strengths(public_data):
171 | expected = {"70:ee:50:36:a9:fc": 31}
172 | assert public_data.get_latest_gust_strengths() == expected
173 |
174 |
175 | def test_public_data_get_average_gust_strength(public_data):
176 | assert public_data.get_average_gust_strength() == 31
177 |
178 |
179 | def test_public_data_get_latest_gust_angles(public_data):
180 | expected = {"70:ee:50:36:a9:fc": 217}
181 | assert public_data.get_latest_gust_angles() == expected
182 |
183 |
184 | def test_public_data_get_locations(public_data):
185 | expected = {
186 | "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169],
187 | "70:ee:50:27:25:b0": [8.7807159, 50.1946167],
188 | "70:ee:50:36:94:7c": [8.791382999999996, 50.2136394],
189 | "70:ee:50:36:a9:fc": [8.801164269110814, 50.19596181704958],
190 | "70:ee:50:01:20:fa": [8.7953, 50.195241],
191 | "70:ee:50:04:ed:7a": [8.785034, 50.192169],
192 | "70:ee:50:27:9f:2c": [8.785342, 50.193573],
193 | "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166],
194 | }
195 | assert public_data.get_locations() == expected
196 |
197 |
198 | def test_public_data_get_time_for_rain_measures(public_data):
199 | expected = {
200 | "70:ee:50:36:a9:fc": 1560248184,
201 | "70:ee:50:1f:68:9e": 1560248344,
202 | "70:ee:50:27:25:b0": 1560247896,
203 | "70:ee:50:36:94:7c": 1560248022,
204 | }
205 | assert public_data.get_time_for_rain_measures() == expected
206 |
207 |
208 | def test_public_data_get_time_for_wind_measures(public_data):
209 | expected = {"70:ee:50:36:a9:fc": 1560248190}
210 | assert public_data.get_time_for_wind_measures() == expected
211 |
212 |
213 | @pytest.mark.parametrize(
214 | "test_input,expected",
215 | [
216 | (
217 | "pressure",
218 | {
219 | "70:ee:50:01:20:fa": 1014.4,
220 | "70:ee:50:04:ed:7a": 1005.4,
221 | "70:ee:50:1f:68:9e": 1007.3,
222 | "70:ee:50:27:25:b0": 1012.8,
223 | "70:ee:50:27:9f:2c": 1010.6,
224 | "70:ee:50:36:94:7c": 1010.6,
225 | "70:ee:50:36:a9:fc": 1010,
226 | "70:ee:50:3c:02:78": 1011.7,
227 | },
228 | ),
229 | (
230 | "temperature",
231 | {
232 | "70:ee:50:01:20:fa": 27.4,
233 | "70:ee:50:04:ed:7a": 19.8,
234 | "70:ee:50:1f:68:9e": 21.1,
235 | "70:ee:50:27:25:b0": 23.2,
236 | "70:ee:50:27:9f:2c": 25.5,
237 | "70:ee:50:36:94:7c": 21.4,
238 | "70:ee:50:36:a9:fc": 20.1,
239 | "70:ee:50:3c:02:78": 23.3,
240 | },
241 | ),
242 | (
243 | "humidity",
244 | {
245 | "70:ee:50:01:20:fa": 58,
246 | "70:ee:50:04:ed:7a": 76,
247 | "70:ee:50:1f:68:9e": 69,
248 | "70:ee:50:27:25:b0": 60,
249 | "70:ee:50:27:9f:2c": 56,
250 | "70:ee:50:36:94:7c": 62,
251 | "70:ee:50:36:a9:fc": 67,
252 | "70:ee:50:3c:02:78": 58,
253 | },
254 | ),
255 | ],
256 | )
257 | def test_public_data_get_latest_station_measures(public_data, test_input, expected):
258 | assert public_data.get_latest_station_measures(test_input) == expected
259 |
260 |
261 | @pytest.mark.parametrize(
262 | "test_input,expected",
263 | [
264 | ("wind_strength", {"70:ee:50:36:a9:fc": 15}),
265 | ("wind_angle", {"70:ee:50:36:a9:fc": 17}),
266 | ("gust_strength", {"70:ee:50:36:a9:fc": 31}),
267 | ("gust_angle", {"70:ee:50:36:a9:fc": 217}),
268 | ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}),
269 | ],
270 | )
271 | def test_public_data_get_accessory_data(public_data, test_input, expected):
272 | assert public_data.get_accessory_data(test_input) == expected
273 |
274 |
275 | @pytest.mark.parametrize(
276 | "test_input,expected",
277 | [
278 | (
279 | {
280 | "70:ee:50:01:20:fa": 1014.4,
281 | "70:ee:50:04:ed:7a": 1005.4,
282 | "70:ee:50:1f:68:9e": 1007.3,
283 | "70:ee:50:27:25:b0": 1012.8,
284 | "70:ee:50:27:9f:2c": 1010.6,
285 | "70:ee:50:36:94:7c": 1010.6,
286 | "70:ee:50:36:a9:fc": 1010,
287 | "70:ee:50:3c:02:78": 1011.7,
288 | },
289 | 1010.35,
290 | ),
291 | (
292 | {
293 | "70:ee:50:01:20:fa": 27.4,
294 | "70:ee:50:04:ed:7a": 19.8,
295 | "70:ee:50:1f:68:9e": 21.1,
296 | "70:ee:50:27:25:b0": 23.2,
297 | "70:ee:50:27:9f:2c": 25.5,
298 | "70:ee:50:36:94:7c": 21.4,
299 | "70:ee:50:36:a9:fc": 20.1,
300 | "70:ee:50:3c:02:78": 23.3,
301 | },
302 | 22.725,
303 | ),
304 | ({}, 0),
305 | ],
306 | )
307 | def test_public_data_average(test_input, expected):
308 | assert pyatmo.public_data.average(test_input) == expected
309 |
--------------------------------------------------------------------------------
/tests/test_pyatmo_weatherstation.py:
--------------------------------------------------------------------------------
1 | """Define tests for WeatherStation module."""
2 | # pylint: disable=protected-access
3 | import json
4 |
5 | import pytest
6 | from freezegun import freeze_time
7 |
8 | import pyatmo
9 |
10 |
11 | def test_weather_station_data(weather_station_data):
12 | assert (
13 | weather_station_data.stations["12:34:56:37:11:ca"]["station_name"]
14 | == "MyStation"
15 | )
16 |
17 |
18 | def test_weather_station_data_no_response(auth, requests_mock):
19 | requests_mock.post(pyatmo.weather_station._GETSTATIONDATA_REQ, text="None")
20 | with pytest.raises(pyatmo.NoDevice):
21 | assert pyatmo.WeatherStationData(auth)
22 |
23 |
24 | def test_weather_station_data_no_body(auth, requests_mock):
25 | with open("fixtures/status_ok.json") as json_file:
26 | json_fixture = json.load(json_file)
27 | requests_mock.post(
28 | pyatmo.weather_station._GETSTATIONDATA_REQ,
29 | json=json_fixture,
30 | headers={"content-type": "application/json"},
31 | )
32 | with pytest.raises(pyatmo.NoDevice):
33 | assert pyatmo.WeatherStationData(auth)
34 |
35 |
36 | def test_weather_station_data_no_data(auth, requests_mock):
37 | with open("fixtures/home_data_empty.json") as json_file:
38 | json_fixture = json.load(json_file)
39 | requests_mock.post(
40 | pyatmo.weather_station._GETSTATIONDATA_REQ,
41 | json=json_fixture,
42 | headers={"content-type": "application/json"},
43 | )
44 | with pytest.raises(pyatmo.NoDevice):
45 | assert pyatmo.WeatherStationData(auth)
46 |
47 |
48 | @pytest.mark.parametrize(
49 | "station_id, expected",
50 | [
51 | (
52 | "12:34:56:37:11:ca",
53 | [
54 | "Garden",
55 | "Kitchen",
56 | "Livingroom",
57 | "NetatmoIndoor",
58 | "NetatmoOutdoor",
59 | "Yard",
60 | ],
61 | ),
62 | (
63 | "12:34:56:36:fd:3c",
64 | ["Module", "NAMain", "Rain Gauge"],
65 | ),
66 | pytest.param(
67 | "NoValidStation",
68 | None,
69 | marks=pytest.mark.xfail(
70 | reason="Invalid station names are not handled yet.",
71 | ),
72 | ),
73 | ],
74 | )
75 | def test_weather_station_get_module_names(weather_station_data, station_id, expected):
76 | assert sorted(weather_station_data.get_module_names(station_id)) == expected
77 |
78 |
79 | @pytest.mark.parametrize(
80 | "station_id, expected",
81 | [
82 | (
83 | None,
84 | {},
85 | ),
86 | (
87 | "12:34:56:37:11:ca",
88 | {
89 | "12:34:56:03:1b:e4": {
90 | "id": "12:34:56:03:1b:e4",
91 | "module_name": "Garden",
92 | "station_name": "MyStation",
93 | },
94 | "12:34:56:05:51:20": {
95 | "id": "12:34:56:05:51:20",
96 | "module_name": "Yard",
97 | "station_name": "MyStation",
98 | },
99 | "12:34:56:07:bb:0e": {
100 | "id": "12:34:56:07:bb:0e",
101 | "module_name": "Livingroom",
102 | "station_name": "MyStation",
103 | },
104 | "12:34:56:07:bb:3e": {
105 | "id": "12:34:56:07:bb:3e",
106 | "module_name": "Kitchen",
107 | "station_name": "MyStation",
108 | },
109 | "12:34:56:36:fc:de": {
110 | "id": "12:34:56:36:fc:de",
111 | "module_name": "NetatmoOutdoor",
112 | "station_name": "MyStation",
113 | },
114 | "12:34:56:37:11:ca": {
115 | "id": "12:34:56:37:11:ca",
116 | "module_name": "NetatmoIndoor",
117 | "station_name": "MyStation",
118 | },
119 | },
120 | ),
121 | (
122 | "12:34:56:1d:68:2e",
123 | {
124 | "12:34:56:1d:68:2e": {
125 | "id": "12:34:56:1d:68:2e",
126 | "module_name": "Basisstation",
127 | "station_name": "NAMain",
128 | },
129 | },
130 | ),
131 | (
132 | "12:34:56:58:c8:54",
133 | {
134 | "12:34:56:58:c8:54": {
135 | "id": "12:34:56:58:c8:54",
136 | "module_name": "NAMain",
137 | "station_name": "Njurunda (Indoor)",
138 | },
139 | "12:34:56:58:e6:38": {
140 | "id": "12:34:56:58:e6:38",
141 | "module_name": "NAModule1",
142 | "station_name": "Njurunda (Indoor)",
143 | },
144 | },
145 | ),
146 | pytest.param(
147 | "NoValidStation",
148 | None,
149 | marks=pytest.mark.xfail(
150 | reason="Invalid station names are not handled yet.",
151 | ),
152 | ),
153 | ],
154 | )
155 | def test_weather_station_get_modules(weather_station_data, station_id, expected):
156 | assert weather_station_data.get_modules(station_id) == expected
157 |
158 |
159 | def test_weather_station_get_station(weather_station_data):
160 | result = weather_station_data.get_station("12:34:56:37:11:ca")
161 |
162 | assert result["_id"] == "12:34:56:37:11:ca"
163 | assert result["station_name"] == "MyStation"
164 | assert result["module_name"] == "NetatmoIndoor"
165 | assert result["type"] == "NAMain"
166 | assert result["data_type"] == [
167 | "Temperature",
168 | "CO2",
169 | "Humidity",
170 | "Noise",
171 | "Pressure",
172 | ]
173 |
174 | assert weather_station_data.get_station("NoValidStation") == {}
175 |
176 |
177 | @pytest.mark.parametrize(
178 | "mid, expected",
179 | [
180 | ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"),
181 | ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"),
182 | ("", {}),
183 | (None, {}),
184 | ],
185 | )
186 | def test_weather_station_get_module(weather_station_data, mid, expected):
187 | mod = weather_station_data.get_module(mid)
188 |
189 | assert isinstance(mod, dict) is True
190 | assert mod.get("_id", mod) == expected
191 |
192 |
193 | @pytest.mark.parametrize(
194 | "module_id, expected",
195 | [
196 | (
197 | "12:34:56:07:bb:3e",
198 | [
199 | "CO2",
200 | "Humidity",
201 | "Temperature",
202 | "battery_percent",
203 | "battery_vp",
204 | "reachable",
205 | "rf_status",
206 | "temp_trend",
207 | ],
208 | ),
209 | (
210 | "12:34:56:07:bb:3e",
211 | [
212 | "CO2",
213 | "Humidity",
214 | "Temperature",
215 | "battery_percent",
216 | "battery_vp",
217 | "reachable",
218 | "rf_status",
219 | "temp_trend",
220 | ],
221 | ),
222 | (
223 | "12:34:56:03:1b:e4",
224 | [
225 | "GustAngle",
226 | "GustStrength",
227 | "WindAngle",
228 | "WindStrength",
229 | "battery_percent",
230 | "battery_vp",
231 | "reachable",
232 | "rf_status",
233 | ],
234 | ),
235 | (
236 | "12:34:56:05:51:20",
237 | [
238 | "Rain",
239 | "battery_percent",
240 | "battery_vp",
241 | "reachable",
242 | "rf_status",
243 | "sum_rain_1",
244 | "sum_rain_24",
245 | ],
246 | ),
247 | (
248 | "12:34:56:37:11:ca",
249 | [
250 | "CO2",
251 | "Humidity",
252 | "Noise",
253 | "Pressure",
254 | "Temperature",
255 | "pressure_trend",
256 | "reachable",
257 | "temp_trend",
258 | "wifi_status",
259 | ],
260 | ),
261 | (
262 | "12:34:56:58:c8:54",
263 | [
264 | "CO2",
265 | "Humidity",
266 | "Noise",
267 | "Pressure",
268 | "Temperature",
269 | "pressure_trend",
270 | "reachable",
271 | "temp_trend",
272 | "wifi_status",
273 | ],
274 | ),
275 | (
276 | "12:34:56:58:e6:38",
277 | [
278 | "Humidity",
279 | "Temperature",
280 | "battery_percent",
281 | "battery_vp",
282 | "reachable",
283 | "rf_status",
284 | "temp_trend",
285 | ],
286 | ),
287 | pytest.param(
288 | None,
289 | None,
290 | marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."),
291 | ),
292 | ],
293 | )
294 | def test_weather_station_get_monitored_conditions(
295 | weather_station_data,
296 | module_id,
297 | expected,
298 | ):
299 | assert sorted(weather_station_data.get_monitored_conditions(module_id)) == expected
300 |
301 |
302 | @freeze_time("2019-06-11")
303 | @pytest.mark.parametrize(
304 | "station_id, exclude, expected",
305 | [
306 | (
307 | "12:34:56:05:51:20",
308 | None,
309 | {},
310 | ),
311 | (
312 | "12:34:56:37:11:ca",
313 | None,
314 | [
315 | "12:34:56:03:1b:e4",
316 | "12:34:56:05:51:20",
317 | "12:34:56:07:bb:0e",
318 | "12:34:56:07:bb:3e",
319 | "12:34:56:36:fc:de",
320 | "12:34:56:37:11:ca",
321 | ],
322 | ),
323 | ("", None, {}),
324 | ("NoValidStation", None, {}),
325 | (
326 | "12:34:56:37:11:ca",
327 | 1000000,
328 | [
329 | "12:34:56:03:1b:e4",
330 | "12:34:56:05:51:20",
331 | "12:34:56:07:bb:0e",
332 | "12:34:56:07:bb:3e",
333 | "12:34:56:36:fc:de",
334 | "12:34:56:37:11:ca",
335 | ],
336 | ),
337 | (
338 | "12:34:56:37:11:ca",
339 | 798103,
340 | [
341 | "12:34:56:03:1b:e4",
342 | "12:34:56:05:51:20",
343 | "12:34:56:07:bb:3e",
344 | "12:34:56:36:fc:de",
345 | "12:34:56:37:11:ca",
346 | ],
347 | ),
348 | ],
349 | )
350 | def test_weather_station_get_last_data(
351 | weather_station_data,
352 | station_id,
353 | exclude,
354 | expected,
355 | ):
356 | mod = weather_station_data.get_last_data(station_id, exclude=exclude)
357 | if mod:
358 | assert sorted(mod) == expected
359 | else:
360 | assert mod == expected
361 |
362 |
363 | @freeze_time("2019-06-11")
364 | @pytest.mark.parametrize(
365 | "station_id, delay, expected",
366 | [
367 | (
368 | "12:34:56:37:11:ca",
369 | 3600,
370 | [
371 | "12:34:56:03:1b:e4",
372 | "12:34:56:05:51:20",
373 | "12:34:56:07:bb:0e",
374 | "12:34:56:07:bb:3e",
375 | "12:34:56:36:fc:de",
376 | "12:34:56:37:11:ca",
377 | ],
378 | ),
379 | (
380 | "12:34:56:37:11:ca",
381 | 798500,
382 | [],
383 | ),
384 | pytest.param(
385 | "NoValidStation",
386 | 3600,
387 | None,
388 | marks=pytest.mark.xfail(reason="Invalid station name not handled yet"),
389 | ),
390 | ],
391 | )
392 | def test_weather_station_check_not_updated(
393 | weather_station_data,
394 | station_id,
395 | delay,
396 | expected,
397 | ):
398 | mod = weather_station_data.check_not_updated(station_id, delay)
399 | assert sorted(mod) == expected
400 |
401 |
402 | @freeze_time("2019-06-11")
403 | @pytest.mark.parametrize(
404 | "station_id, delay, expected",
405 | [
406 | (
407 | "12:34:56:37:11:ca",
408 | 798500,
409 | [
410 | "12:34:56:03:1b:e4",
411 | "12:34:56:05:51:20",
412 | "12:34:56:07:bb:0e",
413 | "12:34:56:07:bb:3e",
414 | "12:34:56:36:fc:de",
415 | "12:34:56:37:11:ca",
416 | ],
417 | ),
418 | (
419 | "12:34:56:37:11:ca",
420 | 100,
421 | [],
422 | ),
423 | ],
424 | )
425 | def test_weather_station_check_updated(
426 | weather_station_data,
427 | station_id,
428 | delay,
429 | expected,
430 | ):
431 | mod = weather_station_data.check_updated(station_id, delay)
432 | if mod:
433 | assert sorted(mod) == expected
434 | else:
435 | assert mod == expected
436 |
437 |
438 | @freeze_time("2019-06-11")
439 | @pytest.mark.parametrize(
440 | "device_id, scale, module_type, expected",
441 | [("MyStation", "scale", "type", [28.1])],
442 | )
443 | def test_weather_station_get_data(
444 | weather_station_data,
445 | requests_mock,
446 | device_id,
447 | scale,
448 | module_type,
449 | expected,
450 | ):
451 | with open("fixtures/weatherstation_measure.json") as json_file:
452 | json_fixture = json.load(json_file)
453 | requests_mock.post(
454 | pyatmo.weather_station._GETMEASURE_REQ,
455 | json=json_fixture,
456 | headers={"content-type": "application/json"},
457 | )
458 | assert (
459 | weather_station_data.get_data(device_id, scale, module_type)["body"][
460 | "1544558433"
461 | ]
462 | == expected
463 | )
464 |
465 |
466 | def test_weather_station_get_last_data_measurements(weather_station_data):
467 | station_id = "12:34:56:37:11:ca"
468 | module_id = "12:34:56:03:1b:e4"
469 |
470 | mod = weather_station_data.get_last_data(station_id, None)
471 |
472 | assert mod[station_id]["Temperature"] == 24.6
473 | assert mod[station_id]["Pressure"] == 1017.3
474 | assert mod[module_id]["WindAngle"] == 217
475 | assert mod[module_id]["WindStrength"] == 4
476 | assert mod[module_id]["GustAngle"] == 206
477 | assert mod[module_id]["GustStrength"] == 9
478 |
479 |
480 | @freeze_time("2019-06-11")
481 | @pytest.mark.parametrize(
482 | "station_id, exclude, expected",
483 | [
484 | (
485 | "12:34:56:37:11:ca",
486 | None,
487 | [
488 | "12:34:56:03:1b:e4",
489 | "12:34:56:05:51:20",
490 | "12:34:56:07:bb:0e",
491 | "12:34:56:07:bb:3e",
492 | "12:34:56:36:fc:de",
493 | "12:34:56:37:11:ca",
494 | ],
495 | ),
496 | (
497 | None,
498 | None,
499 | {},
500 | ),
501 | (
502 | "12:34:56:00:aa:01",
503 | None,
504 | {},
505 | ),
506 | ],
507 | )
508 | def test_weather_station_get_last_data_bug_97(
509 | weather_station_data,
510 | station_id,
511 | exclude,
512 | expected,
513 | ):
514 | mod = weather_station_data.get_last_data(station_id, exclude)
515 | if mod:
516 | assert sorted(mod) == expected
517 | else:
518 | assert mod == expected
519 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py37,py38,pypy3
3 | isolated_build = True
4 | skip_missing_interpreters = True
5 |
6 | [gh-actions]
7 | python =
8 | 3.6: py36
9 | 3.7: py37
10 | 3.8: py38
11 |
12 | [testenv]
13 | deps =
14 | pytest
15 | pytest-cov
16 | pytest-mock
17 | requests-mock
18 | freezegun
19 |
20 | commands =
21 | python -m pytest --cov {envsitepackagesdir}/pyatmo
22 |
23 | [coverage:paths]
24 | source =
25 | pyatmo
26 | .tox/*/lib/python*/site-packages/pyatmo
27 |
28 | [coverage:run]
29 | branch = True
30 | omit = */__main__.py
31 | source =
32 | pyatmo
33 |
34 | [coverage:report]
35 | show_missing = True
36 | precision = 2
37 |
--------------------------------------------------------------------------------