googlewifiapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Eddie Reasoner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: googlewifiapi
3
+ Version: 0.1.0
4
+ Summary: Asynchronous Python wrapper for the local Google Wifi JSON API
5
+ Author-email: Eddie Reasoner <eddie.reasoner@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/EReaso/GoogleWifiApi
8
+ Project-URL: Bug Tracker, https://github.com/EReaso/GoogleWifiApi/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
14
+ Requires-Dist: aiohttp<3.14.0,>=3.11.0
15
+ Requires-Dist: mashumaro>=3.22
16
+ Requires-Dist: yarl>=1.24.5
17
+ Dynamic: license-file
18
+
19
+ # googlewifiapi
20
+
21
+ An asynchronous Python client for reading status from the local JSON API exposed
22
+ by Google Wifi routers.
23
+ The package is designed for integrations such as Home Assistant, but can be used by any Python application that needs router status data.
24
+
25
+ ## About this README
26
+
27
+ This README was generated by AI with human review.
28
+ All code is either written by a human or is thoroughly reviewed.
29
+ I'll try to keep the README updated, but sometimes I might forget.
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.13 or newer
34
+ - A Google Wifi router reachable on the local network
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install googlewifiapi
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Pass an `aiohttp.ClientSession` to the client so your application controls the
45
+ session lifecycle:
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ import aiohttp
51
+
52
+ from googlewifiapi import GoogleWifiAPI
53
+
54
+
55
+ async def main() -> None:
56
+ async with aiohttp.ClientSession() as session:
57
+ router = GoogleWifiAPI(host="192.168.86.1", sess=session)
58
+ await router.async_update()
59
+
60
+ if router.data is not None:
61
+ print(router.data.wan.online)
62
+ print(router.data.software.software_version)
63
+ print(router.data.system.last_restart)
64
+
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ The host defaults to `192.168.86.1`. After a successful update:
70
+
71
+ - `router.raw_data` contains the raw JSON response.
72
+ - `router.data` contains a validated, typed `GoogleWifiStatus`.
73
+ - IP address fields are parsed as `IPv4Address` or `IPv6Address` objects.
74
+ - `system.last_restart` is calculated from the router's uptime.
75
+
76
+ The status object exposes `dns`, `software`, `system`, and `wan` sections. Their
77
+ fields use Python names such as `software_version`, `gateway_ip_address`, and
78
+ `ip_prefix_length`, while the client handles the router API's camelCase keys.
79
+
80
+ ## Errors
81
+
82
+ All library errors inherit from `GoogleWifiException`:
83
+
84
+ - `GoogleWifiClientError` indicates a request, connection, timeout, or JSON
85
+ decoding failure.
86
+ - `GoogleWifiDataValidationError` indicates that the response does not match
87
+ the expected status schema.
88
+
89
+ ## Development
90
+
91
+ Install the test dependencies and run the test suite with [uv](https://docs.astral.sh/uv/):
92
+
93
+ ```bash
94
+ uv sync --group test
95
+ uv run pytest
96
+ ```
97
+
98
+ ## License
99
+
100
+ This project is licensed under the MIT License. See [LICENSE.txt](LICENSE.txt).
101
+
102
+ ## Disclaimer
103
+
104
+ This is an independent, community-developed project and is not affiliated with
105
+ or endorsed by Google LLC. It relies on a local API that Google may change or
106
+ remove without notice.
@@ -0,0 +1,88 @@
1
+ # googlewifiapi
2
+
3
+ An asynchronous Python client for reading status from the local JSON API exposed
4
+ by Google Wifi routers.
5
+ The package is designed for integrations such as Home Assistant, but can be used by any Python application that needs router status data.
6
+
7
+ ## About this README
8
+
9
+ This README was generated by AI with human review.
10
+ All code is either written by a human or is thoroughly reviewed.
11
+ I'll try to keep the README updated, but sometimes I might forget.
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.13 or newer
16
+ - A Google Wifi router reachable on the local network
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install googlewifiapi
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ Pass an `aiohttp.ClientSession` to the client so your application controls the
27
+ session lifecycle:
28
+
29
+ ```python
30
+ import asyncio
31
+
32
+ import aiohttp
33
+
34
+ from googlewifiapi import GoogleWifiAPI
35
+
36
+
37
+ async def main() -> None:
38
+ async with aiohttp.ClientSession() as session:
39
+ router = GoogleWifiAPI(host="192.168.86.1", sess=session)
40
+ await router.async_update()
41
+
42
+ if router.data is not None:
43
+ print(router.data.wan.online)
44
+ print(router.data.software.software_version)
45
+ print(router.data.system.last_restart)
46
+
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ The host defaults to `192.168.86.1`. After a successful update:
52
+
53
+ - `router.raw_data` contains the raw JSON response.
54
+ - `router.data` contains a validated, typed `GoogleWifiStatus`.
55
+ - IP address fields are parsed as `IPv4Address` or `IPv6Address` objects.
56
+ - `system.last_restart` is calculated from the router's uptime.
57
+
58
+ The status object exposes `dns`, `software`, `system`, and `wan` sections. Their
59
+ fields use Python names such as `software_version`, `gateway_ip_address`, and
60
+ `ip_prefix_length`, while the client handles the router API's camelCase keys.
61
+
62
+ ## Errors
63
+
64
+ All library errors inherit from `GoogleWifiException`:
65
+
66
+ - `GoogleWifiClientError` indicates a request, connection, timeout, or JSON
67
+ decoding failure.
68
+ - `GoogleWifiDataValidationError` indicates that the response does not match
69
+ the expected status schema.
70
+
71
+ ## Development
72
+
73
+ Install the test dependencies and run the test suite with [uv](https://docs.astral.sh/uv/):
74
+
75
+ ```bash
76
+ uv sync --group test
77
+ uv run pytest
78
+ ```
79
+
80
+ ## License
81
+
82
+ This project is licensed under the MIT License. See [LICENSE.txt](LICENSE.txt).
83
+
84
+ ## Disclaimer
85
+
86
+ This is an independent, community-developed project and is not affiliated with
87
+ or endorsed by Google LLC. It relies on a local API that Google may change or
88
+ remove without notice.
@@ -0,0 +1,5 @@
1
+ from .api import GoogleWifiAPI
2
+ from .const import LOGGER
3
+ from .status import GoogleWifiStatus
4
+
5
+ __all__ = ["GoogleWifiAPI", "GoogleWifiStatus", "LOGGER"]
@@ -0,0 +1,57 @@
1
+ """Google Wifi API wrapper."""
2
+
3
+ from typing import Any
4
+
5
+ import aiohttp
6
+ from yarl import URL
7
+
8
+ from .const import DEFAULT_HOST, ENDPOINT
9
+ from .exception import (
10
+ GoogleWifiClientError,
11
+ GoogleWifiDataValidationError,
12
+ GoogleWifiException,
13
+ )
14
+ from .status import GoogleWifiStatus
15
+
16
+
17
+ class GoogleWifiAPI:
18
+ """Get the latest data and update the states."""
19
+
20
+ raw_data: dict[str, Any] | None = None
21
+ available: bool = True
22
+ _resource: URL
23
+ data: GoogleWifiStatus | None = None
24
+ sess: aiohttp.ClientSession
25
+
26
+ def __init__(
27
+ self, host: str = DEFAULT_HOST, sess: aiohttp.ClientSession | None = None
28
+ ) -> None:
29
+ """Initialize the API wrapper."""
30
+ self._resource = URL(f"http://{host}{ENDPOINT}")
31
+ self.sess = sess or aiohttp.ClientSession()
32
+
33
+ async def async_update(self) -> GoogleWifiStatus:
34
+ """Get the latest data from the router."""
35
+ try:
36
+ resp = await self.sess.get(self._resource)
37
+ raw_data = await resp.json()
38
+ except (aiohttp.ClientError, ValueError) as err:
39
+ raise GoogleWifiClientError() from err
40
+
41
+ if not isinstance(raw_data, dict):
42
+ raise GoogleWifiDataValidationError(
43
+ f"The router provided data that could not be validated. "
44
+ f"Raw data: {raw_data}"
45
+ )
46
+
47
+ self.raw_data = raw_data
48
+ try:
49
+ self.data = GoogleWifiStatus.from_dict(raw_data)
50
+ return self.data
51
+ except (LookupError, TypeError, ValueError) as err:
52
+ raise GoogleWifiDataValidationError(
53
+ f"The router provided data that could not be validated. "
54
+ f"Raw data: {raw_data}"
55
+ ) from err
56
+ except Exception as err:
57
+ raise GoogleWifiException() from err
@@ -0,0 +1,7 @@
1
+ """Constants for the Google Wifi API wrapper."""
2
+
3
+ import logging
4
+
5
+ DEFAULT_HOST = "192.168.86.1"
6
+ ENDPOINT = "/api/v1/status"
7
+ LOGGER = logging.getLogger(__package__)
@@ -0,0 +1,13 @@
1
+ """Exceptions for the Google Wifi API Wrapper."""
2
+
3
+
4
+ class GoogleWifiException(Exception):
5
+ """Raised when there is a generic exception while wrapping the API."""
6
+
7
+
8
+ class GoogleWifiDataValidationError(GoogleWifiException):
9
+ """Raised when the data cannot be validated."""
10
+
11
+
12
+ class GoogleWifiClientError(GoogleWifiException):
13
+ """Raised when the connection fails in some way."""
@@ -0,0 +1,93 @@
1
+ """Google Wifi Status using mashumaro dataclasses."""
2
+
3
+ import ipaddress
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timedelta
6
+ from typing import Literal, Union
7
+
8
+ from mashumaro import DataClassDictMixin
9
+ from mashumaro.config import BaseConfig
10
+
11
+
12
+ IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class GoogleWifiStatus(DataClassDictMixin):
17
+ """Google Wifi status."""
18
+
19
+ dns: "GoogleWifiStatus.DNS"
20
+ software: "GoogleWifiStatus.Software"
21
+ system: "GoogleWifiStatus.System"
22
+ wan: "GoogleWifiStatus.WAN"
23
+
24
+ class Config(BaseConfig):
25
+ serialize_by_alias = True
26
+ deserialize_by_alias = True
27
+
28
+ @dataclass(frozen=True)
29
+ class DNS(DataClassDictMixin):
30
+ mode: Literal["automatic", "custom"]
31
+ servers: list[IPAddress]
32
+
33
+ @dataclass(frozen=True)
34
+ class Software(DataClassDictMixin):
35
+ software_version: str
36
+ update_status: str
37
+ update_required: bool
38
+ update_progress: float
39
+
40
+ class Config(BaseConfig):
41
+ serialize_by_alias = True
42
+ deserialize_by_alias = True
43
+ aliases = {
44
+ "software_version": "softwareVersion",
45
+ "update_status": "updateStatus",
46
+ "update_required": "updateRequired",
47
+ "update_progress": "updateProgress",
48
+ }
49
+
50
+ @dataclass(frozen=True)
51
+ class System(DataClassDictMixin):
52
+ country_code: str
53
+ model_id: str
54
+ uptime: int
55
+ last_restart: datetime = field(init=False)
56
+
57
+ class Config(BaseConfig):
58
+ serialize_by_alias = True
59
+ deserialize_by_alias = True
60
+ aliases = {
61
+ "country_code": "countryCode",
62
+ "model_id": "modelId",
63
+ }
64
+
65
+ def __post_init__(self):
66
+ """Compute last_restart from uptime."""
67
+ object.__setattr__(
68
+ self,
69
+ "last_restart",
70
+ datetime.now() - timedelta(seconds=self.uptime),
71
+ )
72
+
73
+ @dataclass(frozen=True)
74
+ class WAN(DataClassDictMixin):
75
+ online: bool
76
+ ethernet_link: bool
77
+ gateway_ip_address: IPAddress
78
+ local_ip_address: IPAddress
79
+ ip_method: str
80
+ ip_prefix_length: int
81
+ name_servers: list[IPAddress]
82
+
83
+ class Config(BaseConfig):
84
+ serialize_by_alias = True
85
+ deserialize_by_alias = True
86
+ aliases = {
87
+ "ethernet_link": "ethernetLink",
88
+ "gateway_ip_address": "gatewayIpAddress",
89
+ "local_ip_address": "localIpAddress",
90
+ "ip_method": "ipMethod",
91
+ "ip_prefix_length": "ipPrefixLength",
92
+ "name_servers": "nameServers",
93
+ }
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: googlewifiapi
3
+ Version: 0.1.0
4
+ Summary: Asynchronous Python wrapper for the local Google Wifi JSON API
5
+ Author-email: Eddie Reasoner <eddie.reasoner@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/EReaso/GoogleWifiApi
8
+ Project-URL: Bug Tracker, https://github.com/EReaso/GoogleWifiApi/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
14
+ Requires-Dist: aiohttp<3.14.0,>=3.11.0
15
+ Requires-Dist: mashumaro>=3.22
16
+ Requires-Dist: yarl>=1.24.5
17
+ Dynamic: license-file
18
+
19
+ # googlewifiapi
20
+
21
+ An asynchronous Python client for reading status from the local JSON API exposed
22
+ by Google Wifi routers.
23
+ The package is designed for integrations such as Home Assistant, but can be used by any Python application that needs router status data.
24
+
25
+ ## About this README
26
+
27
+ This README was generated by AI with human review.
28
+ All code is either written by a human or is thoroughly reviewed.
29
+ I'll try to keep the README updated, but sometimes I might forget.
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.13 or newer
34
+ - A Google Wifi router reachable on the local network
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install googlewifiapi
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Pass an `aiohttp.ClientSession` to the client so your application controls the
45
+ session lifecycle:
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ import aiohttp
51
+
52
+ from googlewifiapi import GoogleWifiAPI
53
+
54
+
55
+ async def main() -> None:
56
+ async with aiohttp.ClientSession() as session:
57
+ router = GoogleWifiAPI(host="192.168.86.1", sess=session)
58
+ await router.async_update()
59
+
60
+ if router.data is not None:
61
+ print(router.data.wan.online)
62
+ print(router.data.software.software_version)
63
+ print(router.data.system.last_restart)
64
+
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ The host defaults to `192.168.86.1`. After a successful update:
70
+
71
+ - `router.raw_data` contains the raw JSON response.
72
+ - `router.data` contains a validated, typed `GoogleWifiStatus`.
73
+ - IP address fields are parsed as `IPv4Address` or `IPv6Address` objects.
74
+ - `system.last_restart` is calculated from the router's uptime.
75
+
76
+ The status object exposes `dns`, `software`, `system`, and `wan` sections. Their
77
+ fields use Python names such as `software_version`, `gateway_ip_address`, and
78
+ `ip_prefix_length`, while the client handles the router API's camelCase keys.
79
+
80
+ ## Errors
81
+
82
+ All library errors inherit from `GoogleWifiException`:
83
+
84
+ - `GoogleWifiClientError` indicates a request, connection, timeout, or JSON
85
+ decoding failure.
86
+ - `GoogleWifiDataValidationError` indicates that the response does not match
87
+ the expected status schema.
88
+
89
+ ## Development
90
+
91
+ Install the test dependencies and run the test suite with [uv](https://docs.astral.sh/uv/):
92
+
93
+ ```bash
94
+ uv sync --group test
95
+ uv run pytest
96
+ ```
97
+
98
+ ## License
99
+
100
+ This project is licensed under the MIT License. See [LICENSE.txt](LICENSE.txt).
101
+
102
+ ## Disclaimer
103
+
104
+ This is an independent, community-developed project and is not affiliated with
105
+ or endorsed by Google LLC. It relies on a local API that Google may change or
106
+ remove without notice.
@@ -0,0 +1,14 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ googlewifiapi/__init__.py
5
+ googlewifiapi/api.py
6
+ googlewifiapi/const.py
7
+ googlewifiapi/exception.py
8
+ googlewifiapi/status.py
9
+ googlewifiapi.egg-info/PKG-INFO
10
+ googlewifiapi.egg-info/SOURCES.txt
11
+ googlewifiapi.egg-info/dependency_links.txt
12
+ googlewifiapi.egg-info/requires.txt
13
+ googlewifiapi.egg-info/top_level.txt
14
+ tests/test_status.py
@@ -0,0 +1,3 @@
1
+ aiohttp<3.14.0,>=3.11.0
2
+ mashumaro>=3.22
3
+ yarl>=1.24.5
@@ -0,0 +1 @@
1
+ googlewifiapi
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "googlewifiapi"
7
+ version = "0.1.0"
8
+ description = "Asynchronous Python wrapper for the local Google Wifi JSON API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Eddie Reasoner", email = "eddie.reasoner@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "aiohttp>=3.11.0,<3.14.0",
21
+ "mashumaro>=3.22",
22
+ "yarl>=1.24.5",
23
+ ]
24
+
25
+ [dependency-groups]
26
+ test = [
27
+ "pytest>=9.1.1",
28
+ "pytest-asyncio>=1.4.0",
29
+ "aioresponses>=0.7.9",
30
+ ]
31
+
32
+ [project.urls]
33
+ "Homepage" = "https://github.com/EReaso/GoogleWifiApi"
34
+ "Bug Tracker" = "https://github.com/EReaso/GoogleWifiApi/issues"
35
+
36
+ [tool.pytest.ini_options]
37
+ asyncio_mode = "auto"
38
+
39
+ [tool.ruff]
40
+ target-version = "py313"
41
+ line-length = 88
42
+
43
+ [tool.ruff.lint]
44
+ select = [
45
+ "B",
46
+ "D",
47
+ "E",
48
+ "F",
49
+ "I",
50
+ "SIM",
51
+ "W",
52
+ ]
53
+
54
+ [tool.ruff.lint.pydocstyle]
55
+ convention = "pep257"
56
+
57
+ [tool.mypy]
58
+ python_version = "3.13"
59
+ show_error_codes = true
60
+ follow_imports = "silent"
61
+ ignore_missing_imports = true
62
+ strict = true
63
+ warn_incomplete_stub = true
64
+ warn_redundant_casts = true
65
+ warn_unused_configs = true
66
+ warn_unused_ignores = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,145 @@
1
+ """Test that the API wrapper can fetch and interpret the router status."""
2
+
3
+ from ipaddress import IPv4Address
4
+ from typing import Any
5
+ from unittest.mock import patch
6
+
7
+ import aiohttp
8
+ import pytest
9
+ from aioresponses import aioresponses
10
+
11
+ from googlewifiapi.api import GoogleWifiAPI
12
+ from googlewifiapi.exception import (
13
+ GoogleWifiClientError,
14
+ GoogleWifiDataValidationError,
15
+ GoogleWifiException,
16
+ )
17
+ from googlewifiapi.status import GoogleWifiStatus
18
+
19
+ from .conftest import RESOURCE_URL
20
+
21
+
22
+ def test_schema_validation(normal_response: dict[str, Any]) -> None:
23
+ """Test that the GoogleWifiStatus schema validates sample data from a normal response correctly."""
24
+ status = GoogleWifiStatus.from_dict(normal_response)
25
+
26
+ assert status.dns.mode == "automatic"
27
+ assert status.dns.servers == [
28
+ IPv4Address("75.75.75.75"),
29
+ IPv4Address("75.75.76.76"),
30
+ ]
31
+
32
+ assert status.software.software_version == "softwareVersion"
33
+ assert status.software.update_status == "idle"
34
+ assert status.software.update_required is False
35
+ assert status.software.update_progress == 0.0
36
+
37
+ assert status.system.country_code == "us"
38
+ assert isinstance(status.system.country_code, str)
39
+ assert status.system.model_id == "modelId"
40
+ assert status.system.uptime == 3600
41
+ assert status.system.last_restart is not None
42
+
43
+ assert status.wan.online is True
44
+ assert status.wan.ethernet_link is True
45
+ assert status.wan.gateway_ip_address == IPv4Address("10.0.0.1")
46
+ assert status.wan.local_ip_address == IPv4Address("10.0.0.10")
47
+ assert status.wan.ip_method == "dhcp"
48
+ assert status.wan.ip_prefix_length == 24
49
+ assert status.wan.name_servers == [
50
+ IPv4Address("75.75.75.75"),
51
+ IPv4Address("75.75.76.76"),
52
+ ]
53
+
54
+
55
+ class TestApiUpdate:
56
+ """Tests whether the API wrapper can update with different conditions."""
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_async_update_success(
60
+ self,
61
+ mock_success: aioresponses,
62
+ client: GoogleWifiAPI,
63
+ normal_response: dict[str, Any],
64
+ ) -> None:
65
+ """Test that a normal response updates the client's data correctly."""
66
+ await client.async_update()
67
+
68
+ assert client.raw_data == normal_response
69
+ assert isinstance(client.data, GoogleWifiStatus)
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_async_update_invalid_data_raises_validation_error(
73
+ self,
74
+ mock_aioresponse: aioresponses,
75
+ client: GoogleWifiAPI,
76
+ normal_response: dict[str, Any],
77
+ ) -> None:
78
+ """Test that a response missing required fields raises a validation error."""
79
+ broken_response = {k: v for k, v in normal_response.items() if k != "wan"}
80
+ mock_aioresponse.get(RESOURCE_URL, status=200, payload=broken_response)
81
+
82
+ with pytest.raises(GoogleWifiDataValidationError):
83
+ await client.async_update()
84
+
85
+ @pytest.mark.asyncio
86
+ async def test_async_update_empty_response_raises_validation_error(
87
+ self, mock_aioresponse: aioresponses, client: GoogleWifiAPI
88
+ ) -> None:
89
+ """Test that an empty/null response body raises a validation error."""
90
+ mock_aioresponse.get(RESOURCE_URL, status=200, payload=None)
91
+
92
+ with pytest.raises(GoogleWifiDataValidationError):
93
+ await client.async_update()
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_async_update_invalid_json_raises_client_error(
97
+ self, mock_aioresponse: aioresponses, client: "GoogleWifiAPI"
98
+ ) -> None:
99
+ """Test that a non-JSON response body raises a client error."""
100
+ mock_aioresponse.get(
101
+ RESOURCE_URL,
102
+ status=200,
103
+ body="not valid json{{{",
104
+ content_type="application/json",
105
+ )
106
+
107
+ with pytest.raises(GoogleWifiClientError):
108
+ await client.async_update()
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_async_update_connection_error_raises_client_error(
112
+ self, mock_unreachable: aioresponses, client: "GoogleWifiAPI"
113
+ ) -> None:
114
+ """Test that a connection failure raises a client error."""
115
+ with pytest.raises(GoogleWifiClientError):
116
+ await client.async_update()
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_async_update_timeout_raises_client_error(
120
+ self, mock_aioresponse: aioresponses, client: "GoogleWifiAPI"
121
+ ) -> None:
122
+ """Test that a request timeout raises a client error."""
123
+ mock_aioresponse.get(
124
+ RESOURCE_URL,
125
+ exception=aiohttp.ServerTimeoutError("request timed out"),
126
+ )
127
+
128
+ with pytest.raises(GoogleWifiClientError):
129
+ await client.async_update()
130
+
131
+ @pytest.mark.asyncio
132
+ async def test_async_update_unexpected_error_raises_generic_exception(
133
+ self,
134
+ mock_success: aioresponses,
135
+ client: GoogleWifiAPI,
136
+ ) -> None:
137
+ """Test that an unexpected failure in schema parsing raises the generic exception."""
138
+ with (
139
+ patch(
140
+ "googlewifiapi.status.GoogleWifiStatus.from_dict",
141
+ side_effect=RuntimeError("boom"),
142
+ ),
143
+ pytest.raises(GoogleWifiException),
144
+ ):
145
+ await client.async_update()