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
23 changes: 2 additions & 21 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5.5.0
with:
python-version: "3.8"
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -29,7 +29,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5.5.0
with:
python-version: "3.8"
python-version: "3.9"
- name: Lint with ruff
run: |
python -m pip install --upgrade pip
Expand All @@ -42,25 +42,6 @@ jobs:
mypy devolo_plc_api
mypy tests || true

test_old:
name: Test with Python 3.8
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.2.2
- name: Set up Python 3.8
uses: actions/setup-python@v5.5.0
with:
python-version: 3.8
check-latest: true
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test]
- name: Test with pytest
run: |
pytest --cov=devolo_plc_api -p no:warnings

test:
name: Test with Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5.5.0
with:
python-version: "3.8"
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ This project implements parts of the devolo PLC devices API in Python. Communica
Defining the system requirements with exact versions typically is difficult. But there is a tested environment:

* Linux
* Python 3.8.12
* pip 20.2.4
* httpx 0.21.0
* protobuf 3.17.3
* segno 1.5.2
* zeroconf 0.70.0
* Python 3.9.22
* pip 25.0.1
* ifaddr 0.2.0
* httpx 0.28.1
* protobuf 5.28.3
* segno 1.6.1
* 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:

* pytest 6.2.5
* pytest-asyncio 0.15.1
* pytest-httpx 0.18
* pytest 7.4.4
* pytest-asyncio 0.26.0
* pytest-httpx 0.35.0
* syrupy 4.9.1

## Versioning

Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The devolo PLC API."""

from importlib.metadata import PackageNotFoundError, version

from .device import Device
Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Clients used to communicate with devolo devices."""

from .protobuf import Protobuf

__all__ = ["Protobuf"]
1 change: 1 addition & 0 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Google Protobuf client."""

from __future__ import annotations

import asyncio
Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/device_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The devolo device API."""

import re

from .deviceapi import DeviceApi
Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/device_api/deviceapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Implementation of the devolo device API."""

from __future__ import annotations

import functools
Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Exceptions used by the package."""

from .device import DeviceNotFound, DevicePasswordProtected, DeviceUnavailable
from .feature import FeatureNotSupported

Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Helper methods to allow advanced usage of information provided by the device."""

from io import BytesIO
from typing import Any

Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/plcnet_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The devolo plcnet API."""

from .getnetworkoverview_pb2 import GetNetworkOverview
from .plcnetapi import PlcNetApi

Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/plcnet_api/plcnetapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Implementation of the devolo plcnet API."""

from __future__ import annotations

from typing import TYPE_CHECKING
Expand Down
1 change: 1 addition & 0 deletions devolo_plc_api/zeroconf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Zeroconf dataclasses."""

from __future__ import annotations

from dataclasses import dataclass, field
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ asyncio_default_fixture_loop_scope = "function"
[tool.ruff]
exclude = ["*_pb2.py", "*.pyi"]
line-length = 127
target-version = "py38"
target-version = "py39"

[tool.ruff.lint]
ignore = ["ANN401", "COM812", "D203", "D205", "D212", "FBT001", "N818"]
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Unittests for devolo_plc_api."""

from __future__ import annotations

import json
Expand Down
18 changes: 12 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Test configuration."""

from __future__ import annotations

from collections import OrderedDict
from functools import partial
from typing import TYPE_CHECKING, AsyncGenerator, Generator
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, Mock, patch

import pytest
Expand All @@ -16,6 +17,8 @@
from .mocks.zeroconf import MockAsyncServiceInfo, MockServiceBrowser

if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Generator

from syrupy.assertion import SnapshotAssertion

pytest_plugins = [
Expand All @@ -35,9 +38,11 @@ def block_communication() -> Generator[None, None, None]:
"""Block external communication."""
adapter = OrderedDict()
adapter["eth0"] = Adapter(name="eth0", nice_name="eth0", ips=[IP("192.0.2.100", network_prefix=24, nice_name="eth0")])
with patch("devolo_plc_api.device.get_adapters", return_value=adapter.values()), patch(
"devolo_plc_api.device.AsyncZeroconf", AsyncMock
), patch("devolo_plc_api.device.AsyncServiceInfo", MockAsyncServiceInfo):
with (
patch("devolo_plc_api.device.get_adapters", return_value=adapter.values()),
patch("devolo_plc_api.device.AsyncZeroconf", AsyncMock),
patch("devolo_plc_api.device.AsyncServiceInfo", MockAsyncServiceInfo),
):
yield


Expand Down Expand Up @@ -72,8 +77,9 @@ def patch_sleep() -> Generator[AsyncMock, None, None]:
def service_browser(device_type: DeviceType) -> Generator[None, None, None]:
"""Patch mDNS service browser."""
service_browser = partial(MockServiceBrowser, device_type=device_type)
with patch("devolo_plc_api.device.AsyncServiceBrowser", service_browser), patch(
"devolo_plc_api.network.ServiceBrowser", service_browser
with (
patch("devolo_plc_api.device.AsyncServiceBrowser", service_browser),
patch("devolo_plc_api.network.ServiceBrowser", service_browser),
):
yield

Expand Down
5 changes: 4 additions & 1 deletion tests/fixtures/device_api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Fixtures for device API tests."""

from __future__ import annotations

from secrets import randbelow
from typing import TYPE_CHECKING, AsyncGenerator
from typing import TYPE_CHECKING

import pytest
import pytest_asyncio
Expand All @@ -19,6 +20,8 @@
)

if TYPE_CHECKING:
from collections.abc import AsyncGenerator

from tests import TestData


Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/plcnet_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Fixtures for plcnet API tests."""
from typing import AsyncGenerator

from collections.abc import AsyncGenerator

import pytest
import pytest_asyncio
Expand Down
1 change: 1 addition & 0 deletions tests/mocks/zeroconf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Mock methods from the Zeroconf module."""

from __future__ import annotations

import socket
Expand Down
8 changes: 5 additions & 3 deletions tests/test_device.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test communicating with a devolo device."""

from asyncio import AbstractEventLoop
from unittest.mock import AsyncMock, Mock, patch

Expand Down Expand Up @@ -130,9 +131,10 @@ def test_context_manager(self, test_data: TestData):
@pytest.mark.asyncio
async def test_state_change_removed(self, mock_device: Device):
"""Test that service information are not processed on state change to removed."""
with patch("devolo_plc_api.device.Device._retry_zeroconf_info"), patch(
"devolo_plc_api.device.Device._get_service_info"
) as gsi:
with (
patch("devolo_plc_api.device.Device._retry_zeroconf_info"),
patch("devolo_plc_api.device.Device._get_service_info") as gsi,
):
mock_device._state_change(Mock(), PLCNETAPI, PLCNETAPI, ServiceStateChange.Removed)
assert gsi.call_count == 0

Expand Down
1 change: 1 addition & 0 deletions tests/test_deviceapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test communicating with a the device API."""

from __future__ import annotations

import sys
Expand Down
1 change: 1 addition & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test helper methods."""

from syrupy.assertion import SnapshotAssertion

from devolo_plc_api import wifi_qr_code
Expand Down
21 changes: 13 additions & 8 deletions tests/test_network.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test network discovery."""

from socket import inet_aton
from unittest.mock import Mock, patch

Expand All @@ -19,8 +20,9 @@ class TestNetwork:
@pytest.mark.asyncio
async def test_async_discover_network(self, test_data: TestData, mock_info_from_service: Mock):
"""Test discovering the network asynchronously."""
with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch(
"devolo_plc_api.network.Zeroconf.get_service_info", return_value=""
with (
patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser),
patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""),
):
serial_number = test_data.device_info[SERVICE_TYPE].properties["SN"]
mock_info_from_service.return_value = ZeroconfServiceInfo(
Expand All @@ -33,8 +35,9 @@ async def test_async_discover_network(self, test_data: TestData, mock_info_from_

def test_discover_network(self, test_data: TestData, mock_info_from_service: Mock):
"""Test discovering the network synchronously."""
with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch(
"devolo_plc_api.network.Zeroconf.get_service_info", return_value=""
with (
patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser),
patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""),
):
serial_number = test_data.device_info[SERVICE_TYPE].properties["SN"]
mock_info_from_service.return_value = ZeroconfServiceInfo(
Expand All @@ -53,17 +56,19 @@ def test_add_wrong_state(self):

def test_no_devices(self):
"""Test discovery with no devices."""
with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch(
"devolo_plc_api.network.Zeroconf.get_service_info", return_value=None
with (
patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser),
patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=None),
):
discovered = network.discover_network(timeout=0.1)
assert not discovered

@pytest.mark.parametrize("mt", ["2600", "2601"])
def test_hcu(self, test_data: TestData, mt: str, mock_info_from_service: Mock):
"""Test ignoring Home Control Central Units."""
with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch(
"devolo_plc_api.network.Zeroconf.get_service_info", return_value=""
with (
patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser),
patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""),
):
mock_info_from_service.return_value = ZeroconfServiceInfo(address=test_data.ip.encode(), properties={"MT": mt})
discovered = network.discover_network(timeout=0.1)
Expand Down
1 change: 1 addition & 0 deletions tests/test_plcnetapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test communicating with a the plcnet API."""

import sys
from http import HTTPStatus

Expand Down