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
27 changes: 1 addition & 26 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.12
FROM mcr.microsoft.com/devcontainers/python:3.14

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

Expand All @@ -10,31 +10,6 @@ RUN \
&& pipx uninstall mypy \
&& pipx uninstall pylint

RUN \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
# Additional library needed by some tests and accordingly by VScode Tests Discovery
bluez \
ffmpeg \
libudev-dev \
libavformat-dev \
libavcodec-dev \
libavdevice-dev \
libavutil-dev \
libgammu-dev \
libswscale-dev \
libswresample-dev \
libavfilter-dev \
libpcap-dev \
libturbojpeg0 \
libyaml-dev \
libxml2 \
git \
cmake \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Install uv
RUN pip3 install uv

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
workflow_dispatch:

env:
DEFAULT_PYTHON: "3.9"
DEFAULT_PYTHON: "3.14"

jobs:
codespell:
Expand Down
2,000 changes: 664 additions & 1,336 deletions poetry.lock

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ classifiers = [
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Natural Language :: English",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Home Automation",
Expand All @@ -30,12 +27,13 @@ packages = [
]

[tool.poetry.dependencies]
python = "^3.9"
python = ">=3.14,<4.0"
aiohttp = ">=3.8.1"

[tool.poetry.group.dev.dependencies]
aioresponses = ">=0.7.6"
debugpy = "^1.8.1"
httpx = "^0.28.1"
pre-commit = "^3.7.1"
pytest-asyncio = "^0.23.7"
pytest-aiohttp = "^1.0.5"
Expand All @@ -50,6 +48,7 @@ pytest-xdist = "^3.6.1"
pytest = "^8.2.2"
pytest-github-actions-annotate-failures = "^0.2.0"
pylint = "^3.2.3"
respx = "^0.23.1"
ruff = "^0.4.8"
yamllint = "^1.35.1"
codespell = "^2.3.0"
Expand All @@ -62,6 +61,8 @@ Changelog = "https://github.com/jbouwh/incomfort-client/releases"

[tool.pytest.ini_options]
pythonpath = ["src"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
show_missing = true
Expand All @@ -76,7 +77,7 @@ source = ["incomfortclient"]
# free to run mypy on Windows, Linux, or macOS and get consistent
# results.
platform = "linux"
python_version = "3.9"
python_version = "3.14"

# show error messages from unrelated files
follow_imports = "normal"
Expand Down
5 changes: 1 addition & 4 deletions src/incomfortclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
"""Python client library for the InterGas InComfort system (via Lan2RF gateway).

Each Gateway can have up to 8 Heaters (boilers) and each Heater can have 0-2
Expand Down Expand Up @@ -215,7 +212,7 @@ async def heaters(self, force_refresh: bool = False) -> list[Heater]:

try:
heaters = dict(await self._get("heaterlist.json"))[HEATERLIST]
except (aiohttp.ClientError, UnicodeDecodeError) as exc:
except (aiohttp.ClientError, UnicodeDecodeError, TimeoutError) as exc:
raise InvalidGateway from exc

self._heaters = [
Expand Down
54 changes: 10 additions & 44 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
"""Python client library for the InterGas InComfort system (via Lan2RF gateway)."""

from __future__ import annotations
from typing import Any

import aiohttp
from aioresponses import aioresponses
from aiohttp import ClientSession

from incomfortclient import HEATERLIST, Gateway, Heater

HOSTNAME = "192.168.0.1"

SERIAL_NO_0 = "2110f25190"
SERIAL_NO_1 = "2110f25191"
SERIAL_NO_2 = "2303\\02041"
Expand All @@ -23,47 +14,22 @@
)


async def gwy_with_heaterlist(
hostname: str | None, heaterlist: str | dict[str, Any]
) -> Gateway:
async def gwy_with_heaterlist(gateway: str) -> Gateway:
"""Request the heaterlist from a mocked gateway."""

with aioresponses() as mocked:
if hostname:
if isinstance(heaterlist, dict):
mocked.get(
f"http://{hostname}/heaterlist.json",
payload=heaterlist,
)
else:
mocked.get(
f"http://{hostname}/heaterlist.json",
body=heaterlist.encode(),
)

async with aiohttp.ClientSession() as session:
gwy = Gateway(hostname or HOSTNAME, session=session)
await gwy.heaters()
async with ClientSession() as session:
gwy = Gateway(gateway, session=session)
await gwy.heaters()

return gwy


async def heater_with_status(data_heater: Any, heaterlist: Any) -> Heater:
async def heater_with_status(gateway: str) -> Heater:
"""Update the heater status from a mocked gateway."""

with aioresponses() as mocked:
mocked.get(
f"http://{HOSTNAME}/heaterlist.json",
payload=heaterlist,
)
mocked.get(
f"http://{HOSTNAME}/data.json?heater=0",
payload=data_heater,
)

async with aiohttp.ClientSession() as session:
gwy = Gateway(HOSTNAME, session=session)
heaters = await gwy.heaters()
await heaters[0].update()
async with ClientSession() as session:
gwy = Gateway(gateway, session=session)
heaters = await gwy.heaters()
await heaters[0].update()

return heaters[0]
57 changes: 57 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Fixtures for incomfort backend library."""

from collections.abc import Awaitable, Callable
from typing import Any

from aiohttp import web

from aiohttp.test_utils import TestServer
import pytest


@pytest.fixture(name="heater_list_exc")
def mock_heater_list_exception() -> Exception | None:
"""Mock a heater list exception."""
return None


@pytest.fixture(name="heater_list")
def mock_heater_list() -> dict[str, list[str | None]] | str:
"""Mock a heater list."""
return ""


@pytest.fixture(name="data_heater")
def mock_data_heater() -> dict[str, Any]:
"""Mock the heater data."""
return {}


@pytest.fixture(name="gateway")
async def mock_gateway(
aiohttp_server: Callable[[web.Application], Awaitable[TestServer]],
heater_list: dict[str, list[str | None]] | str,
data_heater: dict[str, Any],
heater_list_exc: Exception | None,
) -> str:
"""Mock a gateway."""

routes = web.RouteTableDef()

@routes.get("/heaterlist.json")
async def heater_list_hook(_request: web.Request) -> web.Response:
if heater_list_exc:
raise heater_list_exc
if isinstance(heater_list, str):
return web.Response(body=heater_list)
return web.json_response(heater_list)

@routes.get("/data.json")
async def data_heater_hook(_request: web.Request) -> web.Response:
return web.json_response(data_heater)

app = web.Application()
app.add_routes(routes)

server = await aiohttp_server(app)
return f"{server.host}:{server.port}"
43 changes: 19 additions & 24 deletions tests/test_gateway.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
"""Python client library for the InterGas InComfort system (via Lan2RF gateway)."""

from __future__ import annotations
from aiohttp import ClientError

import pytest
from common import (
GATEWAYS_WITH_HEATER,
HOSTNAME,
SERIAL_NO_0,
SERIAL_NO_1,
SERIAL_NO_2,
Expand Down Expand Up @@ -39,44 +34,44 @@
# pylint: disable=protected-access


@pytest.mark.asyncio
async def test_gateway_invalid() -> None:
"""Test an invalid gateway."""
@pytest.mark.parametrize(
"heater_list_exc", [ClientError, UnicodeDecodeError, TimeoutError]
)
async def test_gateway_invalid(gateway: str) -> None:
"""Test an invalid gateway request."""
try:
await gwy_with_heaterlist(None, GATEWAYS_WITH_HEATER[0])
await gwy_with_heaterlist(gateway)
except InvalidGateway:
return
raise AssertionError


@pytest.mark.asyncio
@pytest.mark.parametrize("index", range(len(GATEWAYS_SANS_HEATERS)))
async def test_heaterlist_empty(index: int) -> None:
@pytest.mark.parametrize("heater_list", GATEWAYS_SANS_HEATERS)
async def test_heaterlist_empty(gateway: str) -> None:
"""Test the gateway with an empty heater list."""
try:
await gwy_with_heaterlist(HOSTNAME, heaterlist=GATEWAYS_SANS_HEATERS[index])
await gwy_with_heaterlist(gateway)
except InvalidHeaterList:
return
raise AssertionError


@pytest.mark.asyncio
@pytest.mark.parametrize("index", range(len(GATEWAYS_WITH_HEATERS)))
async def test_heaterlist_valid(index: int) -> None:
@pytest.mark.parametrize("heater_list", GATEWAYS_WITH_HEATERS)
async def test_heaterlist_valid(gateway: str) -> None:
"""Test the gateway with a valid heater list."""
gwy = await gwy_with_heaterlist(HOSTNAME, heaterlist=GATEWAYS_WITH_HEATERS[index])
gwy = await gwy_with_heaterlist(gateway)

assert gwy._heaters and gwy._heaters[0].serial_no == SERIAL_NO_0
assert len(gwy._heaters) < 2 or gwy._heaters[1].serial_no == SERIAL_NO_1


@pytest.mark.asyncio
async def test_heaterlist_valid_alt_sn() -> None:
@pytest.mark.parametrize(
"heater_list",
['{"heaterlist":' f'["{SERIAL_NO_2}",null,null,null,null,null,null,null]' "}"],
)
async def test_heaterlist_valid_alt_sn(gateway: str) -> None:
"""Test the gateway with a valid heater list."""
heaterlist_response = (
'{"heaterlist":' f'["{SERIAL_NO_2}",null,null,null,null,null,null,null]' "}"
)
gwy = await gwy_with_heaterlist(HOSTNAME, heaterlist=heaterlist_response)
gwy = await gwy_with_heaterlist(gateway)

assert gwy._heaters and gwy._heaters[0].serial_no == SERIAL_NO_2_CORRECTED
assert len(gwy._heaters) < 2 or gwy._heaters[1].serial_no == SERIAL_NO_2_CORRECTED
31 changes: 16 additions & 15 deletions tests/test_heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
#
"""Python client library for the InterGas InComfort system (via Lan2RF gateway)."""

from __future__ import annotations

import pytest
from common import GATEWAYS_WITH_HEATER, SERIAL_NO_0, heater_with_status

Expand Down Expand Up @@ -163,29 +161,32 @@
)


@pytest.mark.asyncio
async def test_heater_sans_rooms() -> None:
@pytest.mark.parametrize(
("data_heater", "heater_list"), [(HEATER_SANS_ROOMS[0], GATEWAYS_WITH_HEATER[0])]
)
async def test_heater_sans_rooms(gateway: str) -> None:
"""Test heater without rooms."""
heater = await heater_with_status(HEATER_SANS_ROOMS[0], GATEWAYS_WITH_HEATER[0])
heater = await heater_with_status(gateway)
assert heater.status == HEATER_SANS_ROOMS[1]
assert len(heater.rooms) == 0


@pytest.mark.asyncio
async def test_heater_with_rooms() -> None:
@pytest.mark.parametrize(
("data_heater", "heater_list"), [(HEATER_WITH_ROOMS[0], GATEWAYS_WITH_HEATER[0])]
)
async def test_heater_with_rooms(gateway: str) -> None:
"""Test heater with rooms."""
heater = await heater_with_status(
HEATER_WITH_ROOMS[0], heaterlist=GATEWAYS_WITH_HEATER[0]
)
heater = await heater_with_status(gateway)
assert heater.status == HEATER_WITH_ROOMS[1]
assert len(heater.rooms) == 1 and heater.rooms[0].status == HEATER_WITH_ROOMS[2]


@pytest.mark.asyncio
async def test_heater_with_rooms_alt() -> None:
@pytest.mark.parametrize(
("data_heater", "heater_list"),
[(HEATER_WITH_ROOMS_ALT[0], GATEWAYS_WITH_HEATER[0])],
)
async def test_heater_with_rooms_alt(gateway: str) -> None:
"""Test heater with rooms."""
heater = await heater_with_status(
HEATER_WITH_ROOMS_ALT[0], heaterlist=GATEWAYS_WITH_HEATER[0]
)
heater = await heater_with_status(gateway)
assert heater.status == HEATER_WITH_ROOMS_ALT[1]
assert len(heater.rooms) == 1 and heater.rooms[0].status == HEATER_WITH_ROOMS_ALT[2]
Loading
Loading