omron-connect-api 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omron_connect_api-0.0.3/.github/release.sh +38 -0
- omron_connect_api-0.0.3/.github/workflows/publish.yml +55 -0
- omron_connect_api-0.0.3/.gitignore +4 -0
- omron_connect_api-0.0.3/LICENSE +21 -0
- omron_connect_api-0.0.3/Makefile +20 -0
- omron_connect_api-0.0.3/PKG-INFO +57 -0
- omron_connect_api-0.0.3/README.md +40 -0
- omron_connect_api-0.0.3/example.py +15 -0
- omron_connect_api-0.0.3/omron_connect_api/__init__.py +5 -0
- omron_connect_api-0.0.3/omron_connect_api/blood_pressure/__init__.py +0 -0
- omron_connect_api-0.0.3/omron_connect_api/blood_pressure/blood_pressure_reading.py +17 -0
- omron_connect_api-0.0.3/omron_connect_api/blood_pressure/blood_pressure_reading_response.py +18 -0
- omron_connect_api-0.0.3/omron_connect_api/omron_connect_api.py +68 -0
- omron_connect_api-0.0.3/omron_connect_api/omron_connect_api_errors.py +10 -0
- omron_connect_api-0.0.3/pyproject.toml +27 -0
- omron_connect_api-0.0.3/requirements.txt +11 -0
- omron_connect_api-0.0.3/test/__init__.py +0 -0
- omron_connect_api-0.0.3/test/blood_pressure/__init__.py +0 -0
- omron_connect_api-0.0.3/test/blood_pressure/test_blood_pressure_reading_response.py +25 -0
- omron_connect_api-0.0.3/test/omron_connect_server_for_test.py +35 -0
- omron_connect_api-0.0.3/test/test_omron_connect_api.py +130 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
increment_version() {
|
|
5
|
+
local delimiter=.
|
|
6
|
+
local array=($(echo "$1" | tr $delimiter '\n'))
|
|
7
|
+
array[$2]=$((array[$2]+1))
|
|
8
|
+
if [ $2 -lt 2 ]; then array[2]=0; fi
|
|
9
|
+
if [ $2 -lt 1 ]; then array[1]=0; fi
|
|
10
|
+
echo $(local IFS=$delimiter ; echo "${array[*]}")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
apply_bump() {
|
|
14
|
+
if [ "$1" == "patch" ]; then
|
|
15
|
+
increment_version "$2" 2
|
|
16
|
+
elif [ "$1" == "minor" ]; then
|
|
17
|
+
increment_version "$2" 1
|
|
18
|
+
elif [ "$1" == "major" ]; then
|
|
19
|
+
increment_version "$2" 0
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
change_pyproject() {
|
|
24
|
+
sed -i '' "s/${1}/${2}/g" pyproject.toml
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
git_tag () {
|
|
28
|
+
git commit -am "${1}"
|
|
29
|
+
git tag "${1}" -am "${1}"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
BUMP="$1"
|
|
33
|
+
CURRENT="$(grep 'version' pyproject.toml | cut -d\ -f3 | tr -d '"')"
|
|
34
|
+
NEXT="$(apply_bump "$BUMP" "$CURRENT")"
|
|
35
|
+
|
|
36
|
+
echo "$BUMP - from $CURRENT to $NEXT"
|
|
37
|
+
change_pyproject "$CURRENT" "$NEXT"
|
|
38
|
+
git_tag "$NEXT"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Build, test and publish
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
build:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- name: Checkout
|
|
9
|
+
uses: actions/checkout@v4
|
|
10
|
+
|
|
11
|
+
- name: Set up Python
|
|
12
|
+
uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: '3.13'
|
|
15
|
+
cache: 'pip'
|
|
16
|
+
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: pip install -r requirements.txt
|
|
19
|
+
|
|
20
|
+
- name: Check lint
|
|
21
|
+
run: ruff check --output-format=github
|
|
22
|
+
|
|
23
|
+
- name: Test
|
|
24
|
+
run: python3 -m unittest
|
|
25
|
+
|
|
26
|
+
- name: Build
|
|
27
|
+
run: python3 -m build
|
|
28
|
+
|
|
29
|
+
- name: Keep artifacts
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: python-package-distributions
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish-to-pypi:
|
|
36
|
+
if: startsWith(github.ref, 'refs/tags/') # publish on tag pushes
|
|
37
|
+
needs:
|
|
38
|
+
- build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
|
|
41
|
+
environment:
|
|
42
|
+
name: release
|
|
43
|
+
url: https://pypi.org/p/omron-connect-api
|
|
44
|
+
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- name: Download artifacts
|
|
50
|
+
uses: actions/download-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
- name: Publish
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 happydev.ca
|
|
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.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.PHONY : venv test coverage build release
|
|
2
|
+
|
|
3
|
+
venv:
|
|
4
|
+
python3 -m venv .venv && \
|
|
5
|
+
source .venv/bin/activate && \
|
|
6
|
+
pip install -r requirements.txt
|
|
7
|
+
|
|
8
|
+
test:
|
|
9
|
+
ruff check && \
|
|
10
|
+
python3 -m unittest
|
|
11
|
+
|
|
12
|
+
coverage:
|
|
13
|
+
coverage run --branch -m unittest
|
|
14
|
+
coverage html
|
|
15
|
+
|
|
16
|
+
build:
|
|
17
|
+
python3 -m build
|
|
18
|
+
|
|
19
|
+
release:
|
|
20
|
+
.github/release.sh ${bump}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omron-connect-api
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Library to communicate with OMRON connect API
|
|
5
|
+
Project-URL: Homepage, https://github.com/happydev-ca/omron-connect-api
|
|
6
|
+
Project-URL: Issues, https://github.com/happydev-ca/omron-connect-api/issues
|
|
7
|
+
Author-email: Francois Perron <fperron@gmail.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Home Automation
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: aiohttp
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
[](https://github.com/happydev-ca/omron-connect-api/actions/workflows/publish.yml)
|
|
19
|
+
|
|
20
|
+
# omron-connect-api
|
|
21
|
+
|
|
22
|
+
Library to communicate with OMRON connect API.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import aiohttp
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
from omron_connect_api import OmronConnectApi
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def run():
|
|
35
|
+
async with aiohttp.ClientSession() as session:
|
|
36
|
+
api = OmronConnectApi(os.environ['EMAIL'], os.environ['PASSWORD'], session)
|
|
37
|
+
blood_pressure_readings = await api.async_get_blood_pressure_readings()
|
|
38
|
+
print(blood_pressure_readings)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
asyncio.run(run())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Build and test locally
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
make venv
|
|
48
|
+
source .venv/bin/activate
|
|
49
|
+
make test
|
|
50
|
+
make build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Release version
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
make release bump=patch|minor|major
|
|
57
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[](https://github.com/happydev-ca/omron-connect-api/actions/workflows/publish.yml)
|
|
2
|
+
|
|
3
|
+
# omron-connect-api
|
|
4
|
+
|
|
5
|
+
Library to communicate with OMRON connect API.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import aiohttp
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from omron_connect_api import OmronConnectApi
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run():
|
|
18
|
+
async with aiohttp.ClientSession() as session:
|
|
19
|
+
api = OmronConnectApi(os.environ['EMAIL'], os.environ['PASSWORD'], session)
|
|
20
|
+
blood_pressure_readings = await api.async_get_blood_pressure_readings()
|
|
21
|
+
print(blood_pressure_readings)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
asyncio.run(run())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Build and test locally
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
make venv
|
|
31
|
+
source .venv/bin/activate
|
|
32
|
+
make test
|
|
33
|
+
make build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Release version
|
|
37
|
+
|
|
38
|
+
```shell
|
|
39
|
+
make release bump=patch|minor|major
|
|
40
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from omron_connect_api import OmronConnectApi
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def run():
|
|
9
|
+
async with aiohttp.ClientSession() as session:
|
|
10
|
+
api = OmronConnectApi(os.environ['EMAIL'], os.environ['PASSWORD'], session)
|
|
11
|
+
blood_pressure_readings = await api.async_get_blood_pressure_readings()
|
|
12
|
+
print(blood_pressure_readings)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
asyncio.run(run())
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TypeAlias
|
|
4
|
+
|
|
5
|
+
mmHg: TypeAlias = int
|
|
6
|
+
bpm: TypeAlias = int
|
|
7
|
+
count: TypeAlias = int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass()
|
|
11
|
+
class BloodPressureReading:
|
|
12
|
+
measurement_date: datetime
|
|
13
|
+
diastolic: mmHg
|
|
14
|
+
systolic: mmHg
|
|
15
|
+
pulse: bpm
|
|
16
|
+
irregular_heart_beats: count
|
|
17
|
+
movements: count
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from .. import BloodPressureReading
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BloodPressureReadingResponse:
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_json(cls, data: Dict[str, Any]) -> BloodPressureReading:
|
|
11
|
+
return BloodPressureReading(
|
|
12
|
+
measurement_date=datetime.fromtimestamp(int(data['measurementDate'])/1000, timezone.utc),
|
|
13
|
+
diastolic=data['diastolic'],
|
|
14
|
+
systolic=data['systolic'],
|
|
15
|
+
pulse=data['pulse'],
|
|
16
|
+
irregular_heart_beats=int(data['countIrregularHeartBeat']),
|
|
17
|
+
movements=data['movementDetect']
|
|
18
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from logging import getLogger, Logger
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from . import OmronConnectAPIError, BloodPressureReading
|
|
8
|
+
from .blood_pressure.blood_pressure_reading_response import BloodPressureReadingResponse
|
|
9
|
+
|
|
10
|
+
LOGGER: Logger = getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OmronConnectApi:
|
|
14
|
+
base_url = 'https://oi-api.ohiomron.com'
|
|
15
|
+
TOKEN_EXPIRED = timedelta(seconds=1)
|
|
16
|
+
|
|
17
|
+
def __init__(self, email_address: str, password: str, session: aiohttp.ClientSession):
|
|
18
|
+
self.email_address = email_address
|
|
19
|
+
self.password = password
|
|
20
|
+
self.session = session
|
|
21
|
+
self.headers = {'Content-Type': 'application/json'}
|
|
22
|
+
self.expires_at = datetime.now() - self.TOKEN_EXPIRED
|
|
23
|
+
self.last_synced_time = 0
|
|
24
|
+
|
|
25
|
+
async def async_authenticate(self) -> None:
|
|
26
|
+
if datetime.now() < self.expires_at:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
url = '/app/login'
|
|
30
|
+
json = {'app': 'OCM', 'country': 'CA', 'emailAddress': self.email_address, 'password': self.password}
|
|
31
|
+
async with await self._post(url, json) as response:
|
|
32
|
+
body = await response.json()
|
|
33
|
+
self.headers['Authorization'] = body['accessToken']
|
|
34
|
+
self.expires_at = datetime.now() + timedelta(seconds=body['expiresIn'])
|
|
35
|
+
|
|
36
|
+
async def async_get_blood_pressure_readings(self) -> List[BloodPressureReading]:
|
|
37
|
+
await self.async_authenticate()
|
|
38
|
+
|
|
39
|
+
url = f'/app/v2/sync/bp?nextpaginationKey=0&lastSyncedTime={self.last_synced_time}&phoneIdentifier='
|
|
40
|
+
async with await self._get(url) as response:
|
|
41
|
+
json = await response.json()
|
|
42
|
+
self.last_synced_time = int(json['lastSyncedTime'])
|
|
43
|
+
return [BloodPressureReadingResponse.from_json(reading) for reading in json['data']]
|
|
44
|
+
|
|
45
|
+
async def _post(self, url: str, json: dict) -> aiohttp.ClientResponse:
|
|
46
|
+
response = await self.session.post(url=f'{self.base_url}{url}', headers=self.headers, json=json)
|
|
47
|
+
await self._log(url, response, self.headers, json)
|
|
48
|
+
await self._raise_on_error(response)
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
async def _get(self, url: str) -> aiohttp.ClientResponse:
|
|
52
|
+
response = await self.session.get(f'{self.base_url}{url}', headers=self.headers)
|
|
53
|
+
await self._log(url, response, self.headers)
|
|
54
|
+
await self._raise_on_error(response)
|
|
55
|
+
return response
|
|
56
|
+
|
|
57
|
+
async def _raise_on_error(self, response: aiohttp.ClientResponse) -> None:
|
|
58
|
+
body = await response.json()
|
|
59
|
+
if body['success']:
|
|
60
|
+
return
|
|
61
|
+
self.expires_at = datetime.now() - self.TOKEN_EXPIRED
|
|
62
|
+
self.headers.pop('Authorization', None)
|
|
63
|
+
raise OmronConnectAPIError(response)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
async def _log(url: str, response: aiohttp.ClientResponse, headers: dict, json: dict = None) -> None:
|
|
67
|
+
LOGGER.debug(
|
|
68
|
+
f"{url} : Request[[ headers=[{headers}] json=[{json}] ]] - Response[[ status=[{response.status}] headers=[{dict(response.headers)}] body=[{await response.text()}] ]]")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OmronConnectAPIError(aiohttp.ClientResponseError):
|
|
5
|
+
def __init__(self, error_response: aiohttp.ClientResponse):
|
|
6
|
+
self.request_info = error_response.request_info
|
|
7
|
+
self.history = error_response.history
|
|
8
|
+
self.status = error_response.status
|
|
9
|
+
self.message = error_response.reason
|
|
10
|
+
self.headers = error_response.headers
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "omron-connect-api"
|
|
3
|
+
version = "0.0.3"
|
|
4
|
+
description = "Library to communicate with OMRON connect API"
|
|
5
|
+
authors = [{ name = "Francois Perron", email = "fperron@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Development Status :: 4 - Beta",
|
|
9
|
+
"Programming Language :: Python :: 3",
|
|
10
|
+
"License :: OSI Approved :: MIT License",
|
|
11
|
+
"Operating System :: OS Independent",
|
|
12
|
+
"Topic :: Home Automation",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
requires-python = ">=3.13"
|
|
16
|
+
dependencies = ["aiohttp"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/happydev-ca/omron-connect-api"
|
|
20
|
+
Issues = "https://github.com/happydev-ca/omron-connect-api/issues"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["omronconnectapi"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from omron_connect_api.blood_pressure.blood_pressure_reading_response import BloodPressureReadingResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestBloodPressureReadingResponse(unittest.TestCase):
|
|
7
|
+
|
|
8
|
+
def test_parses_json(self):
|
|
9
|
+
reading = BloodPressureReadingResponse.from_json(
|
|
10
|
+
{
|
|
11
|
+
'measurementDate': '1747615042000',
|
|
12
|
+
'diastolic': 82,
|
|
13
|
+
'systolic': 103,
|
|
14
|
+
'pulse': 62,
|
|
15
|
+
'countIrregularHeartBeat': '2',
|
|
16
|
+
'movementDetect': 1
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
datetime_in_utc = datetime(2025, 5, 19, 0, 37, 22, tzinfo=timezone.utc)
|
|
20
|
+
self.assertEqual(reading.measurement_date, datetime_in_utc)
|
|
21
|
+
self.assertEqual(reading.diastolic, 82)
|
|
22
|
+
self.assertEqual(reading.systolic, 103)
|
|
23
|
+
self.assertEqual(reading.pulse, 62)
|
|
24
|
+
self.assertEqual(reading.irregular_heart_beats, 2)
|
|
25
|
+
self.assertEqual(reading.movements, 1)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
|
|
3
|
+
from aioresponses import aioresponses
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OmronConnectServerForTest:
|
|
7
|
+
base_url = 'https://oi-api.ohiomron.com'
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.server = aioresponses()
|
|
11
|
+
|
|
12
|
+
def __enter__(self):
|
|
13
|
+
self.server.__enter__()
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def __exit__(self, *args):
|
|
17
|
+
self.server.__exit__(*args)
|
|
18
|
+
|
|
19
|
+
def prepare_login_response(self, body: dict = None, repeat: bool = True) -> str:
|
|
20
|
+
if body is None:
|
|
21
|
+
body = {'success': True, 'accessToken': 'token', 'expiresIn': 3600}
|
|
22
|
+
self.server.post(f'{self.base_url}/app/login', status=HTTPStatus.OK, payload=body, repeat=repeat)
|
|
23
|
+
return body.get('accessToken')
|
|
24
|
+
|
|
25
|
+
def prepare_blood_pressure_readings_response(self, last_synced_time: int = 0, body: dict = None, repeat: bool = True):
|
|
26
|
+
if body is None:
|
|
27
|
+
body = {'success': True, 'data': [], 'lastSyncedTime': 0}
|
|
28
|
+
url = f'/app/v2/sync/bp?nextpaginationKey=0&lastSyncedTime={last_synced_time}&phoneIdentifier='
|
|
29
|
+
self.server.get(f'{self.base_url}{url}', status=HTTPStatus.OK, payload=body, repeat=repeat)
|
|
30
|
+
|
|
31
|
+
def assert_called_with(self, url, method, *args, **kwargs):
|
|
32
|
+
self.server.assert_called_with(f'{self.base_url}{url}', method, *args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def assert_called_once_with(self, url, method, *args, **kwargs):
|
|
35
|
+
self.server.assert_called_once_with(f'{self.base_url}{url}', method, *args, **kwargs)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
|
|
6
|
+
from omron_connect_api import OmronConnectAPIError, OmronConnectApi
|
|
7
|
+
from omron_connect_api.blood_pressure.blood_pressure_reading_response import BloodPressureReadingResponse
|
|
8
|
+
from test.omron_connect_server_for_test import OmronConnectServerForTest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestOmronConnectAPI(unittest.IsolatedAsyncioTestCase):
|
|
12
|
+
email_address = 'user@gmail.com'
|
|
13
|
+
password = 'password'
|
|
14
|
+
|
|
15
|
+
async def test_authenticates_with_email_address_and_password(self):
|
|
16
|
+
with OmronConnectServerForTest() as server:
|
|
17
|
+
server.prepare_login_response({'success': True, 'accessToken': 'any-token', 'expiresIn': 3600})
|
|
18
|
+
|
|
19
|
+
async with aiohttp.ClientSession() as session:
|
|
20
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
21
|
+
await api.async_authenticate()
|
|
22
|
+
|
|
23
|
+
server.assert_called_with(url='/app/login',
|
|
24
|
+
method='POST',
|
|
25
|
+
headers={'Content-Type': 'application/json'},
|
|
26
|
+
json={
|
|
27
|
+
'app': 'OCM',
|
|
28
|
+
'country': 'CA',
|
|
29
|
+
'emailAddress': self.email_address,
|
|
30
|
+
'password': self.password
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
self.assertEqual(api.headers.get('Authorization'), 'any-token')
|
|
34
|
+
self.assertLess(datetime.now() + timedelta(seconds=3600) - api.expires_at, timedelta(seconds=1))
|
|
35
|
+
|
|
36
|
+
async def test_reuse_valid_token(self):
|
|
37
|
+
with OmronConnectServerForTest() as server:
|
|
38
|
+
server.prepare_login_response({'success': True, 'accessToken': 'any-token', 'expiresIn': 3600})
|
|
39
|
+
async with aiohttp.ClientSession() as session:
|
|
40
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
41
|
+
await api.async_authenticate()
|
|
42
|
+
await api.async_authenticate()
|
|
43
|
+
|
|
44
|
+
server.assert_called_once_with(url='/app/login',
|
|
45
|
+
method='POST',
|
|
46
|
+
headers={'Content-Type': 'application/json'},
|
|
47
|
+
json={
|
|
48
|
+
'app': 'OCM',
|
|
49
|
+
'country': 'CA',
|
|
50
|
+
'emailAddress': self.email_address,
|
|
51
|
+
'password': self.password
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
async def test_authenticates_error(self):
|
|
55
|
+
with OmronConnectServerForTest() as server:
|
|
56
|
+
server.prepare_login_response({'success': False})
|
|
57
|
+
|
|
58
|
+
async with aiohttp.ClientSession() as session:
|
|
59
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
60
|
+
|
|
61
|
+
with self.assertRaises(OmronConnectAPIError):
|
|
62
|
+
await api.async_authenticate()
|
|
63
|
+
|
|
64
|
+
async def test_clear_headers_on_error(self):
|
|
65
|
+
with OmronConnectServerForTest() as server:
|
|
66
|
+
server.prepare_login_response({'success': False})
|
|
67
|
+
|
|
68
|
+
async with aiohttp.ClientSession() as session:
|
|
69
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
70
|
+
api.headers['Authorization'] = 'any-token'
|
|
71
|
+
|
|
72
|
+
with self.assertRaises(OmronConnectAPIError):
|
|
73
|
+
await api.async_authenticate()
|
|
74
|
+
self.assertEqual(api.headers.get('Authorization'), None)
|
|
75
|
+
self.assertLess(datetime.now() - api.TOKEN_EXPIRED - api.expires_at, timedelta(seconds=1))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def test_get_blood_pressure_readings(self):
|
|
79
|
+
with OmronConnectServerForTest() as server:
|
|
80
|
+
token = server.prepare_login_response()
|
|
81
|
+
response = {
|
|
82
|
+
'success': True,
|
|
83
|
+
'lastSyncedTime': 1747615042001,
|
|
84
|
+
'data': [
|
|
85
|
+
{
|
|
86
|
+
'measurementDate': '1747615042000',
|
|
87
|
+
'diastolic': 82,
|
|
88
|
+
'systolic': 103,
|
|
89
|
+
'pulse': 62,
|
|
90
|
+
'countIrregularHeartBeat': 2,
|
|
91
|
+
'movementDetect': 1
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
'measurementDate': '1747615062000',
|
|
95
|
+
'diastolic': 72,
|
|
96
|
+
'systolic': 100,
|
|
97
|
+
'pulse': 64,
|
|
98
|
+
'countIrregularHeartBeat': 0,
|
|
99
|
+
'movementDetect': 0
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
server.prepare_blood_pressure_readings_response(0, response)
|
|
104
|
+
async with aiohttp.ClientSession() as session:
|
|
105
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
106
|
+
readings = await api.async_get_blood_pressure_readings()
|
|
107
|
+
|
|
108
|
+
server.assert_called_with(
|
|
109
|
+
url='/app/v2/sync/bp?nextpaginationKey=0&lastSyncedTime=0&phoneIdentifier=',
|
|
110
|
+
method='GET',
|
|
111
|
+
headers={'Content-Type': 'application/json', 'Authorization': token})
|
|
112
|
+
|
|
113
|
+
expected_readings = [BloodPressureReadingResponse.from_json(reading) for reading in response['data']]
|
|
114
|
+
self.assertEqual(readings, expected_readings)
|
|
115
|
+
self.assertEqual(api.last_synced_time, 1747615042001)
|
|
116
|
+
|
|
117
|
+
async def test_get_blood_pressure_readings_from_last_synced_time(self):
|
|
118
|
+
with OmronConnectServerForTest() as server:
|
|
119
|
+
token = server.prepare_login_response()
|
|
120
|
+
server.prepare_blood_pressure_readings_response(last_synced_time=1747615042001)
|
|
121
|
+
|
|
122
|
+
async with aiohttp.ClientSession() as session:
|
|
123
|
+
api = OmronConnectApi(self.email_address, self.password, session)
|
|
124
|
+
api.last_synced_time = 1747615042001
|
|
125
|
+
await api.async_get_blood_pressure_readings()
|
|
126
|
+
|
|
127
|
+
server.assert_called_with(
|
|
128
|
+
url='/app/v2/sync/bp?nextpaginationKey=0&lastSyncedTime=1747615042001&phoneIdentifier=',
|
|
129
|
+
method='GET',
|
|
130
|
+
headers={'Content-Type': 'application/json', 'Authorization': token})
|