├── .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 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 7 | [![GitHub Actions](https://github.com/jabesq/netatmo-api-python/workflows/Python%20package/badge.svg)](https://github.com/jabesq/netatmo-api-python/actions?workflow=Python+package) 8 | [![PyPi](https://img.shields.io/pypi/v/pyatmo.svg)](https://pypi.python.org/pypi/pyatmo) 9 | [![PyPi](https://img.shields.io/pypi/l/pyatmo.svg)](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 | --------------------------------------------------------------------------------