├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── pre-commit-updater.yml │ ├── publish-to-pypi.yml │ ├── release-drafter.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── blueconnect ├── __init__.py ├── api.py └── models.py ├── pylintrc ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py └── test ├── mock_data ├── blue_device.json ├── last_measurements.json ├── swiming_pool.json ├── swimming_pool_blue.json ├── swimming_pool_feed.json ├── swimming_pool_status.json ├── swimming_pools.json ├── user.json └── user_login.json └── test_base.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: "⬆️ Dependencies" 3 | collapse-after: 1 4 | labels: 5 | - "dependencies" 6 | template: | 7 | ## What's Changed 8 | $CHANGES 9 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-updater.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit auto-update 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | auto-update: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Set up Python 11 | uses: actions/setup-python@v3.1.2 12 | with: 13 | python-version: '3.10' 14 | - name: Install pre-commit 15 | run: pip install pre-commit 16 | - name: Run pre-commit autoupdate 17 | run: pre-commit autoupdate 18 | - name: Create Pull Request 19 | uses: peter-evans/create-pull-request@v4.0.2 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | branch: update/pre-commit-autoupdate 23 | title: Auto-update pre-commit hooks 24 | commit-message: Auto-update pre-commit hooks 25 | body: | 26 | Update versions of tools in pre-commit 27 | configs to latest version 28 | labels: dependencies 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish releases to PyPI 2 | 3 | on: 4 | release: 5 | types: [published, prereleased] 6 | 7 | jobs: 8 | build-and-publish: 9 | name: Builds and publishes releases to PyPI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Set up Python 3.x 14 | uses: actions/setup-python@v1 15 | with: 16 | python-version: '3.x' 17 | - name: Install wheel 18 | run: >- 19 | pip install wheel 20 | - name: Build 21 | run: >- 22 | python3 setup.py sdist bdist_wheel 23 | - name: Publish release to PyPI 24 | uses: pypa/gh-action-pypi-publish@master 25 | with: 26 | user: __token__ 27 | password: ${{ secrets.PYPI_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Drafts your next Release notes as Pull Requests are merged into "master" 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Test with Pre-commit 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ['3.9', '3.10'] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v3.1.2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements_dev.txt 29 | pre-commit install-hooks 30 | - name: Lint/test with pre-commit 31 | run: pre-commit run --all-files 32 | - name: Run pylint on changed files 33 | run: | 34 | pylint -rn -sn --rcfile=pylintrc --fail-on=I $(git ls-files '*.py') 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hide sublime text stuff 2 | *.sublime-project 3 | *.sublime-workspace 4 | 5 | # Hide some OS X stuff 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # IntelliJ IDEA 15 | .idea 16 | *.iml 17 | 18 | # pytest 19 | .pytest_cache 20 | .cache 21 | 22 | # GITHUB Proposed Python stuff: 23 | *.py[cod] 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Packages 29 | *.egg 30 | *.egg-info 31 | dist 32 | build 33 | eggs 34 | .eggs 35 | parts 36 | bin 37 | var 38 | sdist 39 | develop-eggs 40 | .installed.cfg 41 | lib 42 | lib64 43 | pip-wheel-metadata 44 | 45 | # Logs 46 | *.log 47 | pip-log.txt 48 | 49 | # Unit test / coverage reports 50 | .coverage 51 | .tox 52 | coverage.xml 53 | nosetests.xml 54 | htmlcov/ 55 | test-reports/ 56 | test-results.xml 57 | test-output.xml 58 | 59 | # Translations 60 | *.mo 61 | 62 | # Mr Developer 63 | .mr.developer.cfg 64 | .project 65 | .pydevproject 66 | 67 | .python-version 68 | 69 | # emacs auto backups 70 | *~ 71 | *# 72 | *.orig 73 | 74 | # venv stuff 75 | pyvenv.cfg 76 | pip-selfcheck.json 77 | venv 78 | .venv 79 | Pipfile* 80 | share/* 81 | Scripts/ 82 | 83 | # vimmy stuff 84 | *.swp 85 | *.swo 86 | tags 87 | ctags.tmp 88 | 89 | # vagrant stuff 90 | virtualization/vagrant/setup_done 91 | virtualization/vagrant/.vagrant 92 | virtualization/vagrant/config 93 | 94 | # Visual Studio Code 95 | .vscode/* 96 | !.vscode/cSpell.json 97 | !.vscode/extensions.json 98 | !.vscode/tasks.json 99 | 100 | # Typing 101 | .mypy_cache 102 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.2.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | args: 13 | - --safe 14 | - --quiet 15 | - repo: https://gitlab.com/pycqa/flake8 16 | rev: 3.9.2 17 | hooks: 18 | - id: flake8 19 | exclude: ^examples/ 20 | additional_dependencies: 21 | - flake8-docstrings==1.3.1 22 | - pydocstyle==4.0.0 23 | - repo: https://github.com/pre-commit/mirrors-isort 24 | rev: v5.10.1 25 | hooks: 26 | - id: isort 27 | exclude: ^examples/ 28 | - repo: https://github.com/pre-commit/mirrors-mypy 29 | rev: v0.942 30 | hooks: 31 | - id: mypy 32 | additional_dependencies: [types-all] 33 | exclude: ^examples/ 34 | - repo: https://github.com/pycqa/pydocstyle 35 | rev: 6.1.1 36 | hooks: 37 | - id: pydocstyle 38 | exclude: ^examples/|^.venv/|^.vscode/ 39 | - repo: local 40 | hooks: 41 | - id: pylint 42 | name: pylint 43 | entry: pylint 44 | language: system 45 | types: [python] 46 | args: ["-rn", "-sn", "--rcfile=pylintrc", "--fail-on=I", "--disable=import-error"] 47 | exclude: ^.venv/|^.vscode/ 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-blueconnect 2 | Unofficial python library for the Blue Riiot / Blue Connect API 3 | -------------------------------------------------------------------------------- /blueconnect/__init__.py: -------------------------------------------------------------------------------- 1 | """Unofficial python library for the Blue Riiot Blue Connect API.""" 2 | 3 | from typing import List 4 | 5 | from .api import BlueConnectApi 6 | from .models import ( 7 | BlueDevice, 8 | SwimmingPool, 9 | SwimmingPoolFeedMessage, 10 | SwimmingPoolMeasurement, 11 | TemperatureUnit, 12 | ) 13 | 14 | 15 | class BlueConnectSimpleAPI: 16 | """Class that provides a common structure around the Blue Connect API for a single/main swimming pool.""" 17 | 18 | def __init__(self, username: str, password: str, language: str = "en") -> None: 19 | """Inititialize the api connection, a valid username and password must be provided.""" 20 | self._api = BlueConnectApi(username, password) 21 | self._language = language 22 | self._temperature_unit = None 23 | self._pool_info = None 24 | self._pool_feed_message = None 25 | self._pool_blue_device = None 26 | self._pool_measurements = [] 27 | 28 | def close(self) -> None: 29 | """Close connection to the api.""" 30 | self._api.close() 31 | 32 | async def fetch_data(self) -> None: 33 | """Fetch latest state from API.""" 34 | user_info = await self._api.get_user() 35 | main_pool_id = user_info.user_preferences.main_swimming_pool_id 36 | self._temperature_unit = user_info.user_preferences.display_temperature_unit 37 | self._pool_info = await self._api.get_swimming_pool(main_pool_id) 38 | self._pool_feed_message = ( 39 | await self._api.get_swimming_pool_feed(main_pool_id, self._language) 40 | ).current_message 41 | blue_devices = await self._api.get_swimming_pool_blue_devices(main_pool_id) 42 | self._pool_blue_device = blue_devices[0] if blue_devices else None 43 | if self._pool_blue_device: 44 | self._pool_measurements = ( 45 | await self._api.get_last_measurements( 46 | main_pool_id, self._pool_blue_device.serial 47 | ) 48 | ).measurements 49 | 50 | @property 51 | def pool(self) -> SwimmingPool: 52 | """Return full details of the (main) swimming pool.""" 53 | return self._pool_info 54 | 55 | @property 56 | def temperature_unit(self) -> TemperatureUnit: 57 | """Return temperature unit of the temperature measurements.""" 58 | return self._temperature_unit 59 | 60 | @property 61 | def feed_message(self) -> SwimmingPoolFeedMessage: 62 | """Return the (latest) feed/health message for the (main) swimming pool.""" 63 | return self._pool_feed_message 64 | 65 | @property 66 | def blue_device(self) -> BlueDevice: 67 | """Return Blue Connect device info for the (main) swimming pool.""" 68 | return self._pool_blue_device 69 | 70 | @property 71 | def measurements(self) -> List[SwimmingPoolMeasurement]: 72 | """Return all last/current measurements for the (main) swimming pool.""" 73 | return self._pool_measurements 74 | -------------------------------------------------------------------------------- /blueconnect/api.py: -------------------------------------------------------------------------------- 1 | """Unofficial python library for the Blue Riiot Blue Connect API.""" 2 | 3 | import asyncio 4 | import time 5 | from typing import List, Optional 6 | 7 | import aiohttp 8 | from aws_request_signer import AwsRequestSigner 9 | 10 | from .models import ( 11 | BlueDevice, 12 | SwimmingPool, 13 | SwimmingPoolFeed, 14 | SwimmingPoolLastMeasurements, 15 | SwimmingPoolStatus, 16 | User, 17 | ) 18 | 19 | AWS_REGION = "eu-west-1" 20 | AWS_SERVICE = "execute-api" 21 | BASE_HEADERS = { 22 | "User-Agent": "BlueConnect/3.2.1", 23 | "Accept-Language": "en-DK;q=1.0, da-DK;q=0.9", 24 | "Accept": "*/*", 25 | } 26 | BASE_URL = "https://api.riiotlabs.com/prod/" 27 | 28 | 29 | class BlueConnectApi: 30 | """Class that holds the connection to the Blue Connect API.""" 31 | 32 | def __init__(self, username: str, password: str) -> None: 33 | """Inititialize the api connection, a valid username and password must be provided.""" 34 | self._username = username 35 | self._password = password 36 | self._http_session = aiohttp.ClientSession(connector=aiohttp.TCPConnector()) 37 | self._token_info = {} 38 | 39 | def close(self) -> None: 40 | """Close connection to the api.""" 41 | if asyncio.get_event_loop().is_running(): 42 | asyncio.create_task(self.close_async()) 43 | else: 44 | asyncio.get_event_loop().run_until_complete(self.close_async()) 45 | 46 | async def close_async(self) -> None: 47 | """Close connection to the api.""" 48 | await self._http_session.close() 49 | 50 | async def get_user(self) -> User: 51 | """Retrieve details of logged-in user.""" 52 | data = await self.__get_data("user") 53 | return User.from_json(data) 54 | 55 | async def get_blue_device(self, blue_device_serial: str) -> BlueDevice: 56 | """Retrieve details for a specific blue device.""" 57 | data = await self.__get_data(f"blue/{blue_device_serial}") 58 | return BlueDevice.from_json(data) 59 | 60 | async def get_swimming_pools(self) -> List[SwimmingPool]: 61 | """Retrieve all swimming pools.""" 62 | data = await self.__get_data("swimming_pool") 63 | return [SwimmingPool.from_json(item["swimming_pool"]) for item in data["data"]] 64 | 65 | async def get_swimming_pool(self, swimming_pool_id: str) -> SwimmingPool: 66 | """Retrieve details for a specific swimming pool.""" 67 | data = await self.__get_data(f"swimming_pool/{swimming_pool_id}") 68 | return SwimmingPool.from_json(data) 69 | 70 | async def get_swimming_pool_status( 71 | self, swimming_pool_id: str 72 | ) -> SwimmingPoolStatus: 73 | """Retrieve status for a specific swimming pool.""" 74 | data = await self.__get_data(f"swimming_pool/{swimming_pool_id}/status") 75 | return SwimmingPoolStatus.from_json(data) 76 | 77 | async def get_swimming_pool_blue_devices( 78 | self, swimming_pool_id: str 79 | ) -> List[BlueDevice]: 80 | """Retrieve Blue devices for a specific swimming pool.""" 81 | data = await self.__get_data(f"swimming_pool/{swimming_pool_id}/blue") 82 | result = [] 83 | for item in data["data"]: 84 | blue_device = await self.get_blue_device(item["blue_device_serial"]) 85 | result.append(blue_device) 86 | return result 87 | 88 | async def get_swimming_pool_feed( 89 | self, swimming_pool_id: str, language: str = "en" 90 | ) -> SwimmingPoolFeed: 91 | """Retrieve feed for a specific swimming pool.""" 92 | data = await self.__get_data( 93 | f"swimming_pool/{swimming_pool_id}/feed?lang={language}" 94 | ) 95 | return SwimmingPoolFeed.from_json(data) 96 | 97 | async def get_last_measurements( 98 | self, swimming_pool_id: str, blue_device_serial: str 99 | ) -> SwimmingPoolLastMeasurements: 100 | """Retrieve last measurements for a specific swimming pool.""" 101 | data = await self.__get_data( 102 | f"swimming_pool/{swimming_pool_id}/blue/{blue_device_serial}/lastMeasurements?mode=blue_and_strip" 103 | ) 104 | return SwimmingPoolLastMeasurements.from_json(data) 105 | 106 | async def __get_credentials(self) -> dict: 107 | """Retrieve auth credentials by logging in with username/password.""" 108 | if self._token_info and self._token_info["expires"] > time.time(): 109 | # return cached credentials if still valid 110 | return self._token_info["credentials"] 111 | # perform log-in to get credentials 112 | url = BASE_URL + "user/login" 113 | async with self._http_session.post( 114 | url, json={"email": self._username, "password": self._password} 115 | ) as response: 116 | if response.status != 200: 117 | error_msg = await response.text() 118 | raise Exception(f"Error logging in user: {error_msg}") 119 | result = await response.json() 120 | self._token_info = result 121 | self._token_info["expires"] = time.time() + 3500 122 | return result["credentials"] 123 | 124 | async def __get_data(self, endpoint: str, params: dict = None) -> Optional[dict]: 125 | """Get data from api.""" 126 | if params is None: 127 | params = {} 128 | url = BASE_URL + endpoint 129 | headers = BASE_HEADERS.copy() 130 | # sign the request 131 | creds = await self.__get_credentials() 132 | request_signer = AwsRequestSigner( 133 | AWS_REGION, creds["access_key"], creds["secret_key"], AWS_SERVICE 134 | ) 135 | headers.update(request_signer.sign_with_headers("GET", url, headers)) 136 | headers["X-Amz-Security-Token"] = creds["session_token"] 137 | async with self._http_session.get( 138 | url, headers=headers, params=params, verify_ssl=False 139 | ) as response: 140 | if response.status != 200: 141 | error_msg = await response.text() 142 | raise Exception( 143 | f"Error while retrieving data for endpoint {endpoint}: {error_msg}" 144 | ) 145 | return await response.json() 146 | -------------------------------------------------------------------------------- /blueconnect/models.py: -------------------------------------------------------------------------------- 1 | """Models for Blue Connect API objects.""" 2 | 3 | import json 4 | from dataclasses import dataclass, fields 5 | from datetime import datetime 6 | from enum import Enum 7 | from typing import List 8 | 9 | import inflection 10 | from dateutil.parser import isoparse 11 | 12 | 13 | def transform_value(value, value_type): 14 | """Transform raw api value to correct type.""" 15 | if value_type in (str, int, float, bool): 16 | return value 17 | if value_type == datetime: 18 | return isoparse(value) 19 | if isinstance(value, list): 20 | item_type = value_type.__args__[0] 21 | return [transform_value(item, item_type) for item in value] 22 | if hasattr(value_type, "from_json"): 23 | return value_type.from_json(value) 24 | return value_type(value) 25 | 26 | 27 | class ModelBase: 28 | """Common base for our models.""" 29 | 30 | @classmethod 31 | def from_json(cls, json_obj): 32 | """Raw json/dict values from api to instance attributes.""" 33 | # convert json to python dict 34 | if not isinstance(json_obj, dict): 35 | json_obj = json.loads(json_obj) 36 | # extract needed init values from provided json data 37 | cls_attr = {} 38 | for key, value in json_obj.items(): 39 | # make sure we have snake case attribute names 40 | pythonic_key = inflection.underscore(key) 41 | cls_field = next((x for x in fields(cls) if x.name == pythonic_key), None) 42 | if not cls_field: 43 | continue 44 | cls_attr[pythonic_key] = transform_value(value, cls_field.type) 45 | return cls(**cls_attr) 46 | 47 | 48 | @dataclass 49 | class BlueDevice(ModelBase): 50 | """Model for a Blue Connect device.""" 51 | 52 | serial: str 53 | hw_generation: int 54 | hw_product_type: str 55 | hw_product_name: str 56 | last_measure_ble: datetime 57 | last_measure_sigfox: datetime 58 | battery_low: bool 59 | 60 | 61 | class MeasurementTrend(Enum): 62 | """Enum for a measurement trend.""" 63 | 64 | STABLE = "stable" 65 | INCREASE = "increase" 66 | DECREASE = "decrease" 67 | UNDEFINED = "undefined" 68 | 69 | 70 | @dataclass 71 | class SwimmingPoolMeasurement(ModelBase): 72 | """Model for a swimming pool measurement.""" 73 | 74 | name: str 75 | priority: int 76 | timestamp: datetime 77 | expired: bool 78 | value: float 79 | trend: MeasurementTrend 80 | ok_min: float 81 | ok_max: float 82 | warning_high: float 83 | warning_low: float 84 | gauge_max: float 85 | gauge_min: float 86 | issuer: str 87 | 88 | 89 | @dataclass 90 | class SwimmingPoolLastMeasurements(ModelBase): 91 | """Model for swimming pool last measurements data.""" 92 | 93 | status: str 94 | swimming_pool_id: str 95 | data: List[SwimmingPoolMeasurement] 96 | blue_device_serial: str = "" 97 | last_blue_measure_timestamp: datetime = None 98 | last_strip_timestamp: datetime = None 99 | 100 | @property 101 | def measurements(self): 102 | """Get all (current) measurements.""" 103 | return self.data 104 | 105 | 106 | @dataclass 107 | class UserInfo(ModelBase): 108 | """Model for UserInfo.""" 109 | 110 | user_id: str 111 | first_name: str 112 | last_name: str 113 | email: str 114 | account_type: str 115 | 116 | 117 | class TemperatureUnit(Enum): 118 | """Enum for TemperatureUnit.""" 119 | 120 | CELSIUS = "celsius" 121 | FAHRENHEIT = "fahrenheit" 122 | 123 | 124 | @dataclass 125 | class UserPreferences(ModelBase): 126 | """Model for UserPreferences.""" 127 | 128 | display_temperature_unit: TemperatureUnit 129 | display_unit_system: str 130 | main_swimming_pool_id: str 131 | 132 | 133 | @dataclass 134 | class User(ModelBase): 135 | """Model for a User.""" 136 | 137 | user_info: UserInfo 138 | user_preferences: UserPreferences 139 | 140 | 141 | @dataclass 142 | class SwimmingPool(ModelBase): 143 | """Model for a SwimmingPool.""" 144 | 145 | updated: datetime 146 | swimming_pool_id: str 147 | created: datetime 148 | name: str 149 | last_refresh_status: datetime 150 | 151 | 152 | @dataclass 153 | class SwimmingPoolStatusTask(ModelBase): 154 | """Model for a SwimmingPoolStatusTask.""" 155 | 156 | status_id: str 157 | since: datetime 158 | data: dict 159 | swimming_pool_id: str 160 | created: datetime 161 | task_identifier: str 162 | update_reason: str 163 | order: int 164 | 165 | 166 | @dataclass 167 | class SwimmingPoolStatus(ModelBase): 168 | """Model for SwimmingPoolStatus.""" 169 | 170 | since: datetime 171 | status_id: str 172 | global_status_code: str 173 | swimming_pool_id: str 174 | created: str 175 | update_reason: str 176 | blue_device_serial: str 177 | last_notif_date: str 178 | swimming_pool_name: str 179 | tasks: List[SwimmingPoolStatusTask] 180 | 181 | 182 | @dataclass 183 | class SwimmingPoolFeedMessage(ModelBase): 184 | """Model for a SwimmingPoolFeedMessage.""" 185 | 186 | id: str 187 | title: str 188 | message: str 189 | 190 | 191 | @dataclass 192 | class SwimmingPoolFeed(ModelBase): 193 | """Model for a SwimmingPoolFeedMessage.""" 194 | 195 | swimming_pool_id: str 196 | timestamp: datetime 197 | data: List[SwimmingPoolFeedMessage] 198 | lang: str 199 | 200 | @property 201 | def messages(self): 202 | """Get all feed messages.""" 203 | return self.data 204 | 205 | @property 206 | def current_message(self): 207 | """Return the last/current feedmessage.""" 208 | return self.data[0] if self.data else None 209 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore=tests 3 | ignore-patterns=app_vars 4 | ignore-paths=.vscode 5 | # Use a conservative default here; 2 should speed up most setups and not hurt 6 | # any too bad. Override on command line as appropriate. 7 | jobs=2 8 | persistent=no 9 | suggestion-mode=yes 10 | extension-pkg-whitelist=taglib 11 | 12 | [BASIC] 13 | good-names=id,i,j,k,ex,Run,_,fp,T,ev 14 | 15 | [MESSAGES CONTROL] 16 | # Reasons disabled: 17 | # format - handled by black 18 | # locally-disabled - it spams too much 19 | # duplicate-code - unavoidable 20 | # cyclic-import - doesn't test if both import on load 21 | # abstract-class-little-used - prevents from setting right foundation 22 | # unused-argument - generic callbacks and setup methods create a lot of warnings 23 | # too-many-* - are not enforced for the sake of readability 24 | # too-few-* - same as too-many-* 25 | # abstract-method - with intro of async there are always methods missing 26 | # inconsistent-return-statements - doesn't handle raise 27 | # too-many-ancestors - it's too strict. 28 | # wrong-import-order - isort guards this 29 | # fixme - project is in development phase 30 | disable= 31 | format, 32 | abstract-class-little-used, 33 | abstract-method, 34 | cyclic-import, 35 | duplicate-code, 36 | inconsistent-return-statements, 37 | locally-disabled, 38 | not-context-manager, 39 | too-few-public-methods, 40 | too-many-ancestors, 41 | too-many-arguments, 42 | too-many-branches, 43 | too-many-instance-attributes, 44 | too-many-lines, 45 | too-many-locals, 46 | too-many-public-methods, 47 | too-many-return-statements, 48 | too-many-statements, 49 | too-many-boolean-expressions, 50 | unused-argument, 51 | wrong-import-order, 52 | fixme 53 | # enable useless-suppression temporarily every now and then to clean them up 54 | enable= 55 | use-symbolic-message-instead 56 | 57 | [REPORTS] 58 | score=no 59 | 60 | [REFACTORING] 61 | 62 | # Maximum number of nested blocks for function / method body 63 | max-nested-blocks=15 64 | 65 | [TYPECHECK] 66 | # For attrs 67 | ignored-classes=_CountingAttr 68 | 69 | [FORMAT] 70 | expected-line-ending-format=LF 71 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-request-signer>=1.1 2 | aiohttp>=3.8 3 | python-dateutil==2.8 4 | inflection==0.5.1 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | black==22.3.0 3 | flake8==4.0.1 4 | mypy==0.942 5 | pydocstyle==6.1.1 6 | pylint==2.13.7 7 | pre-commit==2.18.1 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | # To work with Black 4 | max-line-length = 100 5 | # E501: line too long 6 | # W503: Line break occurred before a binary operator 7 | # E203: Whitespace before ':' 8 | # D202 No blank lines allowed after function docstring 9 | # W504 line break after binary operator 10 | ignore = 11 | E501, 12 | W503, 13 | E203, 14 | D202, 15 | W504, 16 | E266 17 | 18 | [isort] 19 | profile = black 20 | multi_line_output = 3 21 | include_trailing_comma = True 22 | force_grid_wrap = 0 23 | use_parentheses = True 24 | line_length = 88 25 | 26 | [mypy] 27 | python_version = 3.9 28 | ignore_errors = true 29 | follow_imports = silent 30 | ignore_missing_imports = true 31 | warn_incomplete_stub = true 32 | warn_redundant_casts = true 33 | warn_unused_configs = true 34 | 35 | [pydocstyle] 36 | add-ignore = D202 37 | 38 | [pylint.master] 39 | ignore=tests 40 | ignore-patterns=app_vars 41 | # Use a conservative default here; 2 should speed up most setups and not hurt 42 | # any too bad. Override on command line as appropriate. 43 | jobs=2 44 | persistent=no 45 | suggestion-mode=yes 46 | extension-pkg-whitelist=taglib,orjson 47 | 48 | [pylint.basic] 49 | good-names=id,i,j,k,ex,Run,_,fp,T,ev 50 | 51 | [pylint.messages_control] 52 | # Reasons disabled: 53 | # format - handled by black 54 | # locally-disabled - it spams too much 55 | # duplicate-code - unavoidable 56 | # cyclic-import - doesn't test if both import on load 57 | # abstract-class-little-used - prevents from setting right foundation 58 | # unused-argument - generic callbacks and setup methods create a lot of warnings 59 | # too-many-* - are not enforced for the sake of readability 60 | # too-few-* - same as too-many-* 61 | # abstract-method - with intro of async there are always methods missing 62 | # inconsistent-return-statements - doesn't handle raise 63 | # too-many-ancestors - it's too strict. 64 | # wrong-import-order - isort guards this 65 | # fixme - project is in development phase 66 | # c-extension-no-member - it was giving me headaches 67 | disable= 68 | format, 69 | abstract-class-little-used, 70 | abstract-method, 71 | cyclic-import, 72 | duplicate-code, 73 | inconsistent-return-statements, 74 | locally-disabled, 75 | not-context-manager, 76 | too-few-public-methods, 77 | too-many-ancestors, 78 | too-many-arguments, 79 | too-many-branches, 80 | too-many-instance-attributes, 81 | too-many-lines, 82 | too-many-locals, 83 | too-many-public-methods, 84 | too-many-return-statements, 85 | too-many-statements, 86 | too-many-boolean-expressions, 87 | unused-argument, 88 | wrong-import-order, 89 | fixme, 90 | c-extension-no-member 91 | 92 | # enable useless-suppression temporarily every now and then to clean them up 93 | enable= 94 | use-symbolic-message-instead 95 | 96 | [pylint.reports] 97 | score=no 98 | 99 | [pylint.refactoring] 100 | # Maximum number of nested blocks for function / method body 101 | max-nested-blocks=15 102 | 103 | [pylint.typecheck] 104 | # For attrs 105 | ignored-classes=_CountingAttr 106 | 107 | [pylint.format] 108 | expected-line-ending-format=LF 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup module for blueconnect.""" 2 | from pathlib import Path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | PROJECT_DIR = Path(__file__).parent.resolve() 7 | README_FILE = PROJECT_DIR / "README.md" 8 | VERSION = "1.0.3" 9 | 10 | 11 | setup( 12 | name="blueconnect", 13 | version=VERSION, 14 | url="https://github.com/marcelveldt/python-blueconnect", 15 | download_url="https://github.com/marcelveldt/python-blueconnect", 16 | author="Marcel van der Veldt", 17 | author_email="m.vanderveldt@outlook.com", 18 | description="Unofficial API client for Blue Riiot/Blue Connect devices.", 19 | long_description=README_FILE.read_text(encoding="utf-8"), 20 | long_description_content_type="text/markdown", 21 | packages=find_packages(exclude=["test.*", "test"]), 22 | python_requires=">=3.9", 23 | include_package_data=True, 24 | zip_safe=False, 25 | classifiers=[ 26 | "Development Status :: 4 - Beta", 27 | "Intended Audience :: Developers", 28 | "Natural Language :: English", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Topic :: Home Automation", 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /test/mock_data/blue_device.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /blue/", 3 | "serial": "1234567890", 4 | "sn": "1234567890", 5 | "hw_generation": 2, 6 | "hw_product_type": "go", 7 | "hw_product_pack": "go", 8 | "hw_product_name": "Blue Connect", 9 | "hw_sf_zone": "rc1", 10 | "hw_sf_country": "NL", 11 | "last_measure_ble": "2020-08-15T10:34:48.511Z", 12 | "last_measure_sigfox": "2020-08-17T17:57:21.000Z", 13 | "state": { 14 | "sleep_state": "awake" 15 | }, 16 | "contract_isBasic": false, 17 | "contract_isGo": true, 18 | "contract_isSwaUs": false, 19 | "contract_isVpcEligible": false, 20 | "contract_servicePlan": "plus", 21 | "contract_isMarketPlaceEligible": false, 22 | "location": { 23 | "support": "none", 24 | "location": "pool" 25 | }, 26 | "hw_has_screw": true, 27 | "flash_settings_to_apply": { 28 | "sigfox_enabled": 1, 29 | "wake_period": 4320 30 | }, 31 | "reseller_channel": "gre_01", 32 | "battery_low": false 33 | } 34 | -------------------------------------------------------------------------------- /test/mock_data/last_measurements.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool//blue//lastMeasurements?mode=blue_and_strip", 3 | "status": "OK", 4 | "last_blue_measure_timestamp": "2020-08-17T17:57:21.000Z", 5 | "blue_device_serial": "1234567890", 6 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 7 | "data": [ 8 | { 9 | "name": "temperature", 10 | "priority": 10, 11 | "timestamp": "2020-08-17T17:57:21.000Z", 12 | "expired": false, 13 | "value": 27.9, 14 | "trend": "decrease", 15 | "ok_min": 20, 16 | "ok_max": 40, 17 | "warning_high": 50, 18 | "warning_low": 5, 19 | "gauge_max": 50, 20 | "gauge_min": 0, 21 | "issuer": "sigfox" 22 | }, 23 | { 24 | "name": "ph", 25 | "priority": 20, 26 | "timestamp": "2020-08-17T17:57:21.000Z", 27 | "expired": false, 28 | "value": 7.6, 29 | "trend": "stable", 30 | "ok_min": 7.2, 31 | "ok_max": 7.6, 32 | "warning_high": 8.4, 33 | "warning_low": 6.6, 34 | "gauge_max": 10, 35 | "gauge_min": 5, 36 | "issuer": "sigfox" 37 | }, 38 | { 39 | "name": "orp", 40 | "priority": 30, 41 | "timestamp": "2020-08-17T17:57:21.000Z", 42 | "expired": false, 43 | "value": 685, 44 | "trend": "stable", 45 | "ok_min": 650, 46 | "ok_max": 750, 47 | "warning_high": 900, 48 | "warning_low": 400, 49 | "gauge_max": 1000, 50 | "gauge_min": 300, 51 | "issuer": "sigfox" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /test/mock_data/swiming_pool.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool/", 3 | "updated": "2020-08-13T23:43:56.998Z", 4 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 5 | "chemical": {}, 6 | "created": "2020-07-31T15:26:41.621Z", 7 | "custom_settings": { 8 | "measure_ranges": { 9 | "temperature": {}, 10 | "ph": {}, 11 | "orp": {}, 12 | "conductivity": {} 13 | } 14 | }, 15 | "sigfox_global_coverage": { 16 | "robustness": 3, 17 | "score": 36, 18 | "margins": [ 19 | 43, 20 | 41, 21 | 39 22 | ], 23 | "redundancy": 3, 24 | "level": 8 25 | }, 26 | "characteristics": { 27 | "volume": { 28 | "width": 3.00, 29 | "length": 6.00, 30 | "depth": 1.50, 31 | "shape": "Rectangle", 32 | "capacity": 27 33 | }, 34 | "uv_sanitizing": {}, 35 | "configuration": { 36 | "_type": "Aboveground", 37 | "construction_year": "2020", 38 | "location": "Outdoor", 39 | "kind": "SwimmingPool", 40 | "waterproofing": "Liner" 41 | }, 42 | "sanitizer": { 43 | "chemical": "Chlorine", 44 | "process": "Manuel" 45 | }, 46 | "equipment": { 47 | "heatings": [ 48 | "Heat Pump" 49 | ], 50 | "protections": [ 51 | "BubbleBlanket" 52 | ], 53 | "cleanings": [ 54 | "Manual" 55 | ], 56 | "filtrations": [ 57 | "Glass" 58 | ] 59 | }, 60 | "geoloc": { 61 | "country": "Netherlands", 62 | "country_code": "NL", 63 | "administrative_area_level_2": { 64 | "long_name": "Example City", 65 | "short_name": "Example City" 66 | }, 67 | "city": "'Example City", 68 | "administrative_area_level_1": { 69 | "long_name": "Example State", 70 | "short_name": "EX" 71 | }, 72 | "lon": 1.000000000000000, 73 | "lat": 1.000000000000000, 74 | "zip_code": "1234" 75 | }, 76 | "ph_regulation": {}, 77 | "filter_pump": { 78 | "is_present": true, 79 | "operating_type": "Scheduled", 80 | "operating_hours": [ 81 | { 82 | "start": "2020-07-31T06:00:00Z", 83 | "end": "2020-07-31T18:00:00Z" 84 | } 85 | ] 86 | }, 87 | "picture": {} 88 | }, 89 | "name": "Example Pool", 90 | "chemistry": { 91 | "activeChemicalPackId": "riiot_generic", 92 | "canSelectChemicalPack": false 93 | }, 94 | "last_refresh_status": "2020-08-17T02:22:30.258Z", 95 | "user_access": { 96 | "role": "FIRST_ADMINISTRATOR", 97 | "canDeleteSwimmingPool": true, 98 | "canModifySwimmingPool": true, 99 | "canCreateInviteToken": true 100 | }, 101 | "data_validation": { 102 | "measure_ranges": { 103 | "ph": { 104 | "min": 6.8, 105 | "max": 8, 106 | "delta": 0.1 107 | }, 108 | "orp": { 109 | "min": 600, 110 | "max": 800, 111 | "delta": 10 112 | }, 113 | "temperature": { 114 | "min": 10, 115 | "max": 45, 116 | "delta": 1 117 | }, 118 | "salinity": { 119 | "min": 0, 120 | "max": 15, 121 | "delta": 0.5 122 | } 123 | }, 124 | "automation_targets": { 125 | "ph": { 126 | "min": 6.8, 127 | "max": 8, 128 | "delta": 0.1 129 | }, 130 | "orp": { 131 | "min": 600, 132 | "max": 800, 133 | "delta": 10 134 | }, 135 | "temperature": { 136 | "min": 10, 137 | "max": 45, 138 | "delta": 1 139 | }, 140 | "salinity": { 141 | "min": 0, 142 | "max": 15, 143 | "delta": 0.5 144 | } 145 | } 146 | }, 147 | "data_default": { 148 | "measure_ranges": { 149 | "ph": { 150 | "custom_applied": false, 151 | "min": 7.2, 152 | "max": 7.6, 153 | "ideal": 7.3 154 | }, 155 | "orp": { 156 | "custom_applied": false, 157 | "min": 650, 158 | "max": 750, 159 | "ideal": 700 160 | }, 161 | "conductivity": { 162 | "custom_applied": false, 163 | "min": 300, 164 | "max": 10000, 165 | "ideal": 1500 166 | }, 167 | "salinity": { 168 | "custom_applied": false, 169 | "min": 4, 170 | "max": 6, 171 | "ideal": 5 172 | }, 173 | "temperature": { 174 | "custom_applied": false, 175 | "min": 20, 176 | "max": 40, 177 | "ideal": 27 178 | } 179 | }, 180 | "automation_targets": { 181 | "ph": { 182 | "custom_applied": false, 183 | "target": 7.3, 184 | "repeat_too_high": false, 185 | "repeat_too_low": false 186 | }, 187 | "orp": { 188 | "custom_applied": false, 189 | "target": 700, 190 | "repeat_too_high": false, 191 | "repeat_too_low": false 192 | }, 193 | "temperature": { 194 | "custom_applied": false, 195 | "target": 27, 196 | "repeat_too_high": false, 197 | "repeat_too_low": false 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /test/mock_data/swimming_pool_blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool//blue", 3 | "data": [ 4 | { 5 | "created": "2020-07-31T15:27:00.299Z", 6 | "blue_device": { 7 | "contract_servicePlan": "plus", 8 | "sleep_state": "awake", 9 | "hw_bundle_extender": false, 10 | "contract_isGo": true, 11 | "hw_region": "eu", 12 | "contract_isBasic": false, 13 | "sf_rc": "rc1", 14 | "wake_period": 4320, 15 | "SN": "1234567890", 16 | "last_measure_message_ble": "2020-08-15T10:34:48.511Z", 17 | "sf_rc_country": "NL", 18 | "hw_type": "go", 19 | "reseller_channel": "gre_01", 20 | "fs_beacon_enabled": true, 21 | "contract_isSwaUs": false, 22 | "last_measure_message_sigfox": "2020-08-17T16:45:20.000Z", 23 | "hw_generation": 2, 24 | "hw_color": "white", 25 | "last_measure_message": "2020-08-17T16:45:20.000Z", 26 | "production_datetime": "2020-05-18T00:00:00.000Z", 27 | "serial": "123456789", 28 | "fs_sigfox_enabled": true, 29 | "contract_isVpcEligible": false, 30 | "location": { 31 | "support": "none", 32 | "location": "pool" 33 | }, 34 | "battery_low": false, 35 | "sigfox_geoloc": { 36 | "country_name": "Netherlands", 37 | "country": "528", 38 | "country_code_iso": "NL", 39 | "timestamp": "2020-08-17T16:45:20.000Z" 40 | } 41 | }, 42 | "user_id": "eu-west-1:07fbfaa0-5012-48d4-a57b-f632d7c019ee", 43 | "blue_device_serial": "123456789", 44 | "role": "IS_OWNER", 45 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /test/mock_data/swimming_pool_feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool//feed?lang=en", 3 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 4 | "timestamp": "2020-08-17T17:32:12.612Z", 5 | "data": [ 6 | { 7 | "id": "SWP_OK", 8 | "title": "Enjoy!", 9 | "message": "Water quality is at the top!" 10 | } 11 | ], 12 | "lang": "en" 13 | } 14 | -------------------------------------------------------------------------------- /test/mock_data/swimming_pool_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool//status", 3 | "since": "2020-08-16T08:21:19.687Z", 4 | "status_id": "7f177af4-6148-4134-abf9-c035da5fc7ec", 5 | "global_status_code": "SP_OK", 6 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 7 | "created": "2020-08-17T16:45:47.712Z", 8 | "tasks": [ 9 | { 10 | "status_id": "7f177af4-6148-4134-abf9-c035da5fc7ec", 11 | "since": "2020-07-31T15:43:51.305Z", 12 | "data": "{\"lastMeasure\":\"2020-08-17T16:45:20.000Z\",\"isSigfox\":true}", 13 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 14 | "created": "2020-08-17T16:45:47.712Z", 15 | "task_identifier": "BLUE_ONLINE", 16 | "update_reason": "newMeasure", 17 | "order": 50 18 | }, 19 | { 20 | "status_id": "7f177af4-6148-4134-abf9-c035da5fc7ec", 21 | "since": "2020-07-31T15:43:51.305Z", 22 | "data": "{\"current_value\":28.2,\"swp_range\":{\"ok_min\":20,\"ok_max\":40,\"warning_high\":50,\"warning_low\":5,\"ideal\":27}}", 23 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 24 | "created": "2020-08-17T16:45:47.712Z", 25 | "task_identifier": "TEMPERATURE_OK", 26 | "update_reason": "newMeasure", 27 | "order": -1 28 | }, 29 | { 30 | "status_id": "7f177af4-6148-4134-abf9-c035da5fc7ec", 31 | "since": "2020-07-31T15:43:51.305Z", 32 | "data": "{\"current_value\":7.6,\"swp_range\":{\"ok_min\":7.2,\"ok_max\":7.6,\"warning_high\":8.4,\"warning_low\":6.6,\"ideal\":7.3}}", 33 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 34 | "created": "2020-08-17T16:45:47.712Z", 35 | "task_identifier": "PH_OK", 36 | "update_reason": "newMeasure", 37 | "order": -1 38 | }, 39 | { 40 | "status_id": "7f177af4-6148-4134-abf9-c035da5fc7ec", 41 | "since": "2020-08-16T08:21:19.687Z", 42 | "data": "{\"current_value\":691,\"swp_range\":{\"ok_min\":650,\"ok_max\":750,\"warning_high\":900,\"warning_low\":400,\"ideal\":700}}", 43 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 44 | "created": "2020-08-17T16:45:47.712Z", 45 | "task_identifier": "ORP_OK", 46 | "update_reason": "newMeasure", 47 | "order": -1 48 | } 49 | ], 50 | "update_reason": "newMeasure", 51 | "blue_device_serial": "123456789", 52 | "last_notif_date": "2020-08-16T08:21:19.764Z", 53 | "swimming_pool_name": "Example Pool" 54 | } 55 | -------------------------------------------------------------------------------- /test/mock_data/swimming_pools.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "call to /swimming_pool", 3 | "data": [ 4 | { 5 | "created": "2020-07-31T15:26:41.621Z", 6 | "user_id": "eu-west-1:07fbfaa0-5012-48d4-a57b-f632d7c019ee", 7 | "swimming_pool": { 8 | "chemistry": { 9 | "activeChemicalPackId": "riiot_generic", 10 | "canSelectChemicalPack": false 11 | }, 12 | "last_refresh_status": "2020-08-17T02:22:30.258Z", 13 | "characteristics": { 14 | "volume": { 15 | "width": 3.00, 16 | "length": 6.00, 17 | "depth": 1.50, 18 | "shape": "Rectangle", 19 | "capacity": 27 20 | }, 21 | "uv_sanitizing": {}, 22 | "configuration": { 23 | "_type": "Aboveground", 24 | "construction_year": "2020", 25 | "location": "Outdoor", 26 | "kind": "SwimmingPool", 27 | "waterproofing": "Liner" 28 | }, 29 | "sanitizer": { 30 | "chemical": "Chlorine", 31 | "process": "Manuel" 32 | }, 33 | "equipment": { 34 | "heatings": [ 35 | "Heat Pump" 36 | ], 37 | "protections": [ 38 | "BubbleBlanket" 39 | ], 40 | "cleanings": [ 41 | "Manual" 42 | ], 43 | "filtrations": [ 44 | "Glass" 45 | ] 46 | }, 47 | "geoloc": { 48 | "country": "Netherlands", 49 | "country_code": "NL", 50 | "administrative_area_level_2": { 51 | "long_name": "Example City", 52 | "short_name": "Example City" 53 | }, 54 | "city": "'Example City", 55 | "administrative_area_level_1": { 56 | "long_name": "Example State", 57 | "short_name": "EX" 58 | }, 59 | "lon": 1.000000000000000, 60 | "lat": 1.000000000000000, 61 | "zip_code": "1234" 62 | }, 63 | "ph_regulation": {}, 64 | "filter_pump": { 65 | "is_present": true, 66 | "operating_type": "Scheduled", 67 | "operating_hours": [ 68 | { 69 | "start": "2020-07-31T06:00:00Z", 70 | "end": "2020-07-31T18:00:00Z" 71 | } 72 | ] 73 | }, 74 | "picture": {} 75 | }, 76 | "chemical": {}, 77 | "created": "2020-07-31T15:26:41.621Z", 78 | "name": "Example Pool", 79 | "custom_settings": { 80 | "measure_ranges": { 81 | "temperature": {}, 82 | "ph": {}, 83 | "orp": {}, 84 | "conductivity": {} 85 | } 86 | }, 87 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db", 88 | "updated": "2020-08-13T23:43:56.998Z", 89 | "sigfox_global_coverage": { 90 | "robustness": 3, 91 | "score": 36, 92 | "margins": [ 93 | 43, 94 | 41, 95 | 39 96 | ], 97 | "redundancy": 3, 98 | "level": 8 99 | } 100 | }, 101 | "role": "FIRST_ADMINISTRATOR", 102 | "name": "Example Pool", 103 | "swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db" 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/mock_data/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "GET call to /url", 3 | "userInfo": { 4 | "userIdentityId": "eu-west-1:07fbfaa0-5012-48d4-a57b-f632d7c019ee", 5 | "userId": "example@example.com", 6 | "firstName": "Example", 7 | "lastName": "Example", 8 | "email": "example@example.com", 9 | "accepted_last_eula": true, 10 | "account_type": "Email" 11 | }, 12 | "userPreferences": { 13 | "display_temperature_unit": "celsius", 14 | "display_unit_system": "metric", 15 | "notifications": { 16 | "user_id": "eu-west-1:07fbfaa0-5012-48d4-a57b-f632d7c019ee", 17 | "alerts_mail_enabled": false, 18 | "measurement_frequency": "every_day", 19 | "alerts_push_enabled": true, 20 | "measurement_mail_enabled": false, 21 | "measurement_hour": 11, 22 | "measurement_push_enabled": false 23 | }, 24 | "main_swimming_pool_id": "20d3be3a-6627-4cba-9939-40fb13d0d1db" 25 | }, 26 | "currentEula": { 27 | "url": "https://s3-eu-west-1.amazonaws.com/riiotlabsassets/eula/1.0/{lang}.html", 28 | "version": "1.0", 29 | "available_languages": [ 30 | "fr", 31 | "es", 32 | "en", 33 | "nl", 34 | "de", 35 | "it", 36 | "pt", 37 | "cs" 38 | ] 39 | }, 40 | "userModuleAccess": { 41 | "va_config": true, 42 | "autom_targets_config": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/mock_data/user_login.json: -------------------------------------------------------------------------------- 1 | { 2 | "__description__": "POST call to /user/login with email/password in json body", 3 | "identity_id": "test", 4 | "token": "test", 5 | "credentials": { 6 | "access_key": "test", 7 | "secret_key": "test", 8 | "session_token": "test", 9 | "expiration": "2020-08-17T16:35:59.000Z" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/test_base.py: -------------------------------------------------------------------------------- 1 | """Tests for Blue Connect api library.""" 2 | 3 | # TODO ! 4 | --------------------------------------------------------------------------------