Skip to content

Commit

Permalink
Merge pull request #31 from 2Fake/v0.5.0
Browse files Browse the repository at this point in the history
v0.5.0
  • Loading branch information
2Fake authored Dec 20, 2020
2 parents c739dd6 + 978b46c commit 23e4bae
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 37 deletions.
2 changes: 1 addition & 1 deletion devolo_plc_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.0"
__version__ = "0.5.0"
10 changes: 7 additions & 3 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from typing import Callable

from google.protobuf.json_format import MessageToDict
from httpx import AsyncClient, DigestAuth, Response
from httpx import AsyncClient, ConnectTimeout, DigestAuth, Response

from ..exceptions.device import DevicePasswordProtected
from ..exceptions.device import DevicePasswordProtected, DeviceUnavailable

TIMEOUT = 5.0

Expand All @@ -19,7 +19,7 @@ class Protobuf(ABC):
@abstractclassmethod
def __init__(self):
self._loop = asyncio.get_running_loop()
self._logger = logging.getLogger(self.__class__.__name__)
self._logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")

self.password: str

Expand Down Expand Up @@ -54,6 +54,8 @@ async def _async_get(self, sub_url: str, timeout: float = TIMEOUT) -> Response:
return await self._session.get(url, auth=DigestAuth(self._user, self.password), timeout=timeout)
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None
except ConnectTimeout:
raise DeviceUnavailable("The device is currenctly not available. Maybe on standby?") from None

async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEOUT) -> Response:
""" Post data asynchronously. """
Expand All @@ -63,6 +65,8 @@ async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEO
return await self._session.post(url, auth=DigestAuth(self._user, self.password), content=content, timeout=timeout)
except TypeError:
raise DevicePasswordProtected("The used password is wrong.") from None
except ConnectTimeout:
raise DeviceUnavailable("The device is currenctly not available. Maybe on standby?") from None

@staticmethod
def _message_to_dict(message) -> dict:
Expand Down
34 changes: 6 additions & 28 deletions devolo_plc_api/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ def __init__(self,
self.device = None
self.plcnet = None

self._connected = False
self._info: Dict = {
"_dvl-plcnetapi._tcp.local.": plcnetapi or EMPTY_INFO,
"_dvl-deviceapi._tcp.local.": deviceapi or EMPTY_INFO,
}
self._logger = logging.getLogger(self.__class__.__name__)
self._logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
self._password = ""
self._session_instance: Optional[httpx.AsyncClient] = None
self._zeroconf_instance = zeroconf_instance
Expand All @@ -59,34 +60,9 @@ def __init__(self,
self._session: httpx.AsyncClient
self._zeroconf: Zeroconf

@staticmethod
async def async_validate_password(ip_address: str, password: str, device: str = "wifi"):
async with httpx.AsyncClient() as client:
if device == "wifi":
headers = {"Content-Type": "application/json"}
json_data = {"username": "root", "password": password, "timeout": 900}
payload = {"id": "8df89bc3-29b7-4d7e-9168-1ef1449aecf0",
"jsonrpc": "2.0",
"method": "call",
"params": ["00000000000000000000000000000000",
"session",
"login",
json_data]}
r = await client.post(f"http://{ip_address}/ubus", headers=headers, json=payload)
return not bool(r.json()["result"][0])
elif device == "lan":
# LAN device
pass
else:
# Unknown device
pass

def __del__(self):
try:
if self._loop.is_running():
self._logger.warning("Please disconnect properly from the device.")
except AttributeError:
pass
if self._connected and self._session_instance is None:
self._logger.warning("Please disconnect properly from the device.")

async def __aenter__(self):
await self.async_connect()
Expand Down Expand Up @@ -127,6 +103,7 @@ async def async_connect(self, session_instance: Optional[httpx.AsyncClient] = No
await asyncio.gather(self._get_device_info(), self._get_plcnet_info())
if not self.device and not self.plcnet:
raise DeviceNotFound(f"The device {self.ip} did not answer.")
self._connected = True

def connect(self):
""" Connect to a device synchronous. """
Expand All @@ -139,6 +116,7 @@ async def async_disconnect(self):
self._zeroconf.close()
if not self._session_instance:
await self._session.aclose()
self._connected = False

def disconnect(self):
""" Disconnect from a device asynchronous. """
Expand Down
4 changes: 4 additions & 0 deletions devolo_plc_api/exceptions/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ class DeviceNotFound(Exception):
""" The device was not found. """


class DeviceUnavailable(Exception):
""" The device is not available, e.g. in standby. """


class DevicePasswordProtected(Exception):
""" The device is passwort protected. """
10 changes: 8 additions & 2 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
-
## [v0.5.0] - 2020/12/21

### Changed

- Increase read timeout to better handle busy devices
- If a device is unavailable (e.g. in standby), DeviceUnavailable is raised
- Loggers now contain the module name

### Fixed

- Sometime a warning popped up to properly close the connection to the device although it was properly closed

## [v0.4.0] - 2020/12/08

Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ def mock_device(mocker, request):
device = Device(ip=request.cls.ip)
device._info = deepcopy(request.cls.device_info)
device._loop = Mock()
device._loop.is_running = lambda: False
device._session = Mock()
device._session.aclose = AsyncMock()
device._zeroconf = Mock()
device._zeroconf.close = lambda: None
return device
yield device
device.disconnect()


@pytest.fixture()
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/protobuf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from httpx import ConnectTimeout

try:
from unittest.mock import AsyncMock
Expand All @@ -23,6 +24,12 @@ def mock_post(mocker):
mocker.patch("httpx.AsyncClient.post", new=AsyncMock())


@pytest.fixture()
def mock_device_unavailable(mocker):
mocker.patch("httpx.AsyncClient.get", side_effect=ConnectTimeout(message="", request=""))
mocker.patch("httpx.AsyncClient.post", side_effect=ConnectTimeout(message="", request=""))


@pytest.fixture()
def mock_wrong_password(mocker):
mocker.patch("httpx.AsyncClient.get", side_effect=TypeError())
Expand Down
3 changes: 3 additions & 0 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ async def test_async_connect(self, mocker, mock_device):
await mock_device.async_connect()
assert spy_device_info.call_count == 1
assert spy_plcnet_info.call_count == 1
assert mock_device._connected

@pytest.mark.asyncio
async def test_async_connect_not_found(self, mock_device):
with patch("devolo_plc_api.device.Device._get_device_info", new=AsyncMock()), \
patch("devolo_plc_api.device.Device._get_plcnet_info", new=AsyncMock()), \
pytest.raises(DeviceNotFound):
await mock_device.async_connect()
assert not mock_device._connected

def test_connect(self, mocker, mock_device):
with patch("devolo_plc_api.device.Device.async_connect", new=AsyncMock()):
Expand All @@ -58,6 +60,7 @@ async def test_async_disconnect(self, mocker, mock_device):
await mock_device.async_disconnect()
assert spy_zeroconf.call_count == 1
assert spy_session.call_count == 1
assert not mock_device._connected

def test_disconnect(self, mocker, mock_device):
with patch("devolo_plc_api.device.Device.async_disconnect", new=AsyncMock()):
Expand Down
16 changes: 15 additions & 1 deletion tests/test_profobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from devolo_plc_api.device_api.devolo_idl_proto_deviceapi_ledsettings_pb2 import LedSettingsSetResponse
from devolo_plc_api.exceptions.device import DevicePasswordProtected
from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable


class TestProtobuf:
Expand Down Expand Up @@ -33,6 +33,13 @@ async def test__async_get_wrong_password(self, mock_protobuf):
with pytest.raises(DevicePasswordProtected):
await mock_protobuf._async_get("LedSettingsGet")

@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_device_unavailable")
async def test__async_get_device_unavailable(self, mock_protobuf):
mock_protobuf._session = httpx.AsyncClient()
with pytest.raises(DeviceUnavailable):
await mock_protobuf._async_get("LedSettingsGet")

def test__message_to_dict(self, mocker, mock_protobuf):
spy = mocker.spy(google.protobuf.json_format._Printer, "_MessageToJsonObject")
mock_protobuf._message_to_dict(LedSettingsSetResponse())
Expand All @@ -52,3 +59,10 @@ async def test__async_post_wrong_password(self, mock_protobuf):
mock_protobuf._session = httpx.AsyncClient()
with pytest.raises(DevicePasswordProtected):
await mock_protobuf._async_post("LedSettingsGet", "")

@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_device_unavailable")
async def test__async_post_device_unavailable(self, mock_protobuf):
mock_protobuf._session = httpx.AsyncClient()
with pytest.raises(DeviceUnavailable):
await mock_protobuf._async_post("LedSettingsGet", "")

0 comments on commit 23e4bae

Please sign in to comment.