Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: '23.9.1'
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: '5.12.0'
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v4.4.0'
hooks:
- id: check-json
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/abravalheri/validate-pyproject
rev: 'v0.14'
hooks:
- id: validate-pyproject
- repo: local
hooks:
- id: stubgen
name: check API stub files
entry: scripts/stubgen.py
description: check if stub files of the APIs are up-to-date
language: script
types: [python]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: "25.1.0"
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: "6.0.1"
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v5.0.0"
hooks:
- id: check-json
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/abravalheri/validate-pyproject
rev: "v0.24.1"
hooks:
- id: validate-pyproject
- repo: local
hooks:
- id: stubgen
name: check API stub files
entry: scripts/stubgen.py
description: check if stub files of the APIs are up-to-date
language: script
types: [python]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Defining the system requirements with exact versions typically is difficult. But
* httpx 0.28.1
* protobuf 5.28.3
* segno 1.6.1
* tenacity 9.0.0
* zeroconf 0.146.1

Other versions and even other operating systems might work. Feel free to tell us about your experience. If you want to run our unit tests, you also need:
Expand Down
8 changes: 8 additions & 0 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
RemoteProtocolError,
Response,
)
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential

from devolo_plc_api.exceptions import DevicePasswordProtected, DeviceUnavailable

Expand Down Expand Up @@ -70,6 +71,13 @@ async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEO
self._logger.debug("Posting to %s", url)
return await self._async_request("POST", url, content, timeout)

@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=5),
retry=retry_if_exception_type(DeviceUnavailable),
reraise=True,
before_sleep=before_sleep_log(logging.getLogger("devolo_plc_api.clients.protobuf.Protobuf"), logging.DEBUG),
)
async def _async_request(self, method: str, url: str, content: bytes | None, timeout: float = TIMEOUT) -> Response:
"""Request data asynchronously."""
try:
Expand Down
9 changes: 6 additions & 3 deletions devolo_plc_api/device_api/deviceapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
_P = ParamSpec("_P")


LONG_RUNNING = 30.0


def _feature(
feature: str,
) -> Callable[[Callable[Concatenate[DeviceApi, _P], _ReturnT]], Callable[Concatenate[DeviceApi, _P], _ReturnT]]:
Expand Down Expand Up @@ -196,7 +199,7 @@ async def async_get_support_info(self) -> SupportInfoDump:
"""
self._logger.debug("Get uptime.")
support_info = SupportInfoDumpResponse()
response = await self._async_get("SupportInfoDump")
response = await self._async_get("SupportInfoDump", timeout=LONG_RUNNING)
support_info.ParseFromString(await response.aread())
return support_info.info

Expand All @@ -209,7 +212,7 @@ async def async_check_firmware_available(self) -> UpdateFirmwareCheck:
"""
self._logger.debug("Checking for new firmware.")
update_firmware_check = UpdateFirmwareCheck()
response = await self._async_get("UpdateFirmwareCheck", timeout=30.0)
response = await self._async_get("UpdateFirmwareCheck", timeout=LONG_RUNNING)
update_firmware_check.ParseFromString(await response.aread())
return update_firmware_check

Expand Down Expand Up @@ -283,7 +286,7 @@ async def async_get_wifi_neighbor_access_points(self) -> list[WifiNeighborAPsGet
"""
self._logger.debug("Getting neighbored access points.")
wifi_neighbor_aps = WifiNeighborAPsGet()
response = await self._async_get("WifiNeighborAPsGet", timeout=30.0)
response = await self._async_get("WifiNeighborAPsGet", timeout=LONG_RUNNING)
wifi_neighbor_aps.ParseFromString(await response.aread())
return list(wifi_neighbor_aps.neighbor_aps)

Expand Down
2 changes: 2 additions & 0 deletions devolo_plc_api/device_api/deviceapi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ from devolo_plc_api.clients import Protobuf
from devolo_plc_api.zeroconf import ZeroconfServiceInfo as ZeroconfServiceInfo
from httpx import AsyncClient as AsyncClient

LONG_RUNNING: float

class DeviceApi(Protobuf):
features: list[str]
password: str
Expand Down
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
## [v1.5.0] - 2025/04/13

- Drop support for Python 3.8
### Added

- Retry mechanism for timed-out connections
- Use SPDX license identifier for project metadata

### Changed

- Drop support for Python 3.8

## [v1.4.1] - 2023/09/14

### Fixed
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"httpx>=0.21.0",
"protobuf>=4.22.0",
"segno>=1.5.2",
"tenacity>=9.0.0",
"zeroconf>=0.70.0",
]
dynamic = [
Expand Down
6 changes: 4 additions & 2 deletions tests/test_deviceapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from http import HTTPStatus
from typing import TYPE_CHECKING
from unittest.mock import patch

import pytest
from httpx import ConnectTimeout
Expand Down Expand Up @@ -341,10 +342,11 @@ async def test_device_unavailable(self, httpx_mock: HTTPXMock, mock_device: Devi
"""Test device being unavailable."""
await mock_device.async_connect()
assert mock_device.device
httpx_mock.add_exception(ConnectTimeout(""))
with pytest.raises(DeviceUnavailable):
httpx_mock.add_exception(ConnectTimeout(""), is_reusable=True)
with pytest.raises(DeviceUnavailable), patch("asyncio.sleep"):
await mock_device.device.async_get_wifi_connected_station()
await mock_device.async_disconnect()
assert len(httpx_mock.get_requests()) == 3

@pytest.mark.asyncio
@pytest.mark.parametrize("device_type", [DeviceType.PLC])
Expand Down
6 changes: 4 additions & 2 deletions tests/test_plcnetapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
from http import HTTPStatus
from unittest.mock import patch

import pytest
from httpx import ConnectTimeout
Expand Down Expand Up @@ -118,10 +119,11 @@ async def test_device_unavailable(self, httpx_mock: HTTPXMock, mock_device: Devi
"""Test device being unavailable."""
await mock_device.async_connect()
assert mock_device.plcnet
httpx_mock.add_exception(ConnectTimeout(""))
with pytest.raises(DeviceUnavailable):
httpx_mock.add_exception(ConnectTimeout(""), is_reusable=True)
with pytest.raises(DeviceUnavailable), patch("asyncio.sleep"):
await mock_device.plcnet.async_get_network_overview()
await mock_device.async_disconnect()
assert len(httpx_mock.get_requests()) == 3

@pytest.mark.asyncio
@pytest.mark.parametrize("device_type", [DeviceType.PLC])
Expand Down