Skip to content

Commit

Permalink
Merge pull request #32 from 2Fake/development
Browse files Browse the repository at this point in the history
Release v0.5.0
  • Loading branch information
2Fake authored Dec 20, 2020
2 parents dc448b2 + 23e4bae commit deb7de0
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 18 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"
12 changes: 8 additions & 4 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
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 = 3.0
TIMEOUT = 5.0


class Protobuf(ABC):
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
7 changes: 5 additions & 2 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 @@ -60,7 +61,7 @@ def __init__(self,
self._zeroconf: Zeroconf

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

async def __aenter__(self):
Expand Down Expand Up @@ -102,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 @@ -114,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. """
18 changes: 10 additions & 8 deletions devolo_plc_api/network/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ def discover_network() -> Dict[str, Device]:

def _add(zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange):
"""" Create a device object to each matching device. """
if state_change is ServiceStateChange.Added:
service_info = zeroconf.get_service_info(service_type, name)
if service_info is None:
return
if state_change is not ServiceStateChange.Added:
return

info = Device.info_from_service(service_info)
if info is None or info["properties"]["MT"] in ("2600", "2601"):
return # Don't react on devolo Home Control central units
service_info = zeroconf.get_service_info(service_type, name)
if service_info is None:
return

_devices[info["properties"]["SN"]] = Device(ip=info["address"], deviceapi=info, zeroconf_instance=zeroconf)
info = Device.info_from_service(service_info)
if info is None or info["properties"]["MT"] in ("2600", "2601"):
return # Don't react on devolo Home Control central units

_devices[info["properties"]["SN"]] = Device(ip=info["address"], deviceapi=info, zeroconf_instance=zeroconf)
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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).

## [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

### Added
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
7 changes: 7 additions & 0 deletions tests/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def test__add(self, mocker):
network._add(Zeroconf(), "_dvl-deviceapi._tcp.local.", "_dvl-deviceapi._tcp.local.", ServiceStateChange.Added)
assert "1234567890123456" in network._devices

def test__add_wrong_state(self, mocker):
with patch("zeroconf.Zeroconf.get_service_info", return_value="service_info"), \
patch("devolo_plc_api.device.Device.info_from_service", return_value=None):
spy_device = mocker.spy(Device, "__init__")
network._add(Zeroconf(), "_dvl-deviceapi._tcp.local.", "_dvl-deviceapi._tcp.local.", ServiceStateChange.Removed)
assert spy_device.call_count == 0

def test__add_no_device(self, mocker):
with patch("zeroconf.Zeroconf.get_service_info", return_value=None):
spy_info = mocker.spy(Device, "info_from_service")
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 deb7de0

Please sign in to comment.