-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #436 from DiamondLightSource/435_create_webcam_device
Create webcam device for robot load snapshots on i03
- Loading branch information
Showing
5 changed files
with
134 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from pathlib import Path | ||
|
||
import aiofiles | ||
from aiohttp import ClientSession | ||
from bluesky.protocols import Triggerable | ||
from ophyd_async.core import AsyncStatus, StandardReadable | ||
|
||
from dodal.devices.ophyd_async_utils import create_soft_signal_rw | ||
from dodal.log import LOGGER | ||
|
||
|
||
class Webcam(StandardReadable, Triggerable): | ||
def __init__(self, name, prefix, url): | ||
self.url = url | ||
self.filename = create_soft_signal_rw(str, "filename", "webcam") | ||
self.directory = create_soft_signal_rw(str, "directory", "webcam") | ||
self.last_saved_path = create_soft_signal_rw(str, "last_saved_path", "webcam") | ||
|
||
self.set_readable_signals([self.last_saved_path]) | ||
super().__init__(name=name) | ||
|
||
@AsyncStatus.wrap | ||
async def trigger(self) -> None: | ||
filename = await self.filename.get_value() | ||
directory = await self.directory.get_value() | ||
|
||
async with ClientSession() as session: | ||
async with session.get(self.url) as response: | ||
response.raise_for_status() | ||
path = Path(f"{directory}/{filename}.png").as_posix() | ||
LOGGER.info(f"Saving webcam image from {self.url} to {path}") | ||
async with aiofiles.open(path, "wb") as file: | ||
await file.write((await response.read())) | ||
await self.last_saved_path.set(path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from unittest.mock import AsyncMock, MagicMock, patch | ||
|
||
import pytest | ||
from bluesky.run_engine import RunEngine | ||
|
||
from dodal.beamlines import i03 | ||
from dodal.devices.webcam import Webcam | ||
|
||
|
||
@pytest.fixture | ||
def webcam() -> Webcam: | ||
RunEngine() | ||
return i03.webcam(fake_with_ophyd_sim=True) | ||
|
||
|
||
async def test_given_last_saved_path_when_device_read_then_returns_path(webcam: Webcam): | ||
await webcam.last_saved_path.set("test") | ||
read = await webcam.read() | ||
assert read["webcam-last_saved_path"]["value"] == "test" | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"directory, filename, expected_path", | ||
[ | ||
("/tmp", "test", "/tmp/test.png"), | ||
("/tmp/", "other", "/tmp/other.png"), | ||
], | ||
) | ||
@patch("dodal.devices.webcam.aiofiles", autospec=True) | ||
@patch("dodal.devices.webcam.ClientSession.get", autospec=True) | ||
async def test_given_filename_and_directory_when_trigger_and_read_then_returns_expected_path( | ||
mock_get: MagicMock, | ||
mock_aiofiles, | ||
directory, | ||
filename, | ||
expected_path, | ||
webcam: Webcam, | ||
): | ||
mock_get.return_value.__aenter__.return_value = AsyncMock() | ||
await webcam.filename.set(filename) | ||
await webcam.directory.set(directory) | ||
await webcam.trigger() | ||
read = await webcam.read() | ||
assert read["webcam-last_saved_path"]["value"] == expected_path | ||
|
||
|
||
@patch("dodal.devices.webcam.aiofiles", autospec=True) | ||
@patch("dodal.devices.webcam.ClientSession.get", autospec=True) | ||
async def test_given_data_returned_from_url_when_trigger_then_data_written( | ||
mock_get: MagicMock, mock_aiofiles, webcam: Webcam | ||
): | ||
mock_get.return_value.__aenter__.return_value = (mock_response := AsyncMock()) | ||
mock_response.read.return_value = (test_web_data := "TEST") | ||
mock_open = mock_aiofiles.open | ||
mock_open.return_value.__aenter__.return_value = (mock_file := AsyncMock()) | ||
await webcam.filename.set("file") | ||
await webcam.directory.set("/tmp") | ||
await webcam.trigger() | ||
mock_open.assert_called_once_with("/tmp/file.png", "wb") | ||
mock_file.write.assert_called_once_with(test_web_data) | ||
|
||
|
||
@patch("dodal.devices.webcam.aiofiles", autospec=True) | ||
@patch("dodal.devices.webcam.ClientSession.get", autospec=True) | ||
async def test_given_response_throws_exception_when_trigger_then_exception_rasied( | ||
mock_get: MagicMock, mock_aiofiles, webcam: Webcam | ||
): | ||
class MyException(Exception): | ||
pass | ||
|
||
def _raise(): | ||
raise MyException() | ||
|
||
mock_get.return_value.__aenter__.return_value = (mock_response := AsyncMock()) | ||
mock_response.raise_for_status = _raise | ||
await webcam.filename.set("file") | ||
await webcam.directory.set("/tmp") | ||
with pytest.raises(MyException): | ||
await webcam.trigger() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters