Skip to content

Commit 53f1f4c

Browse files
committed
feat: Release 0.7.8 - Complete TypedDict integration for Home Assistant
- Enhanced type safety with full TypedDict compliance - DeviceInfoDict, APIVersionResponse, StructuredValuesDict properly used - String literals for TypedDict keys (elegant MyPy compliance) - StaticValues and InstantValues use proper typed interfaces - All tests pass, MyPy strict mode clean - Ready for Home Assistant strict typing requirements
1 parent eb56231 commit 53f1f4c

File tree

9 files changed

+89
-33
lines changed

9 files changed

+89
-33
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.7.8] - 2025-10-27
9+
10+
### Enhanced
11+
12+
- **Type Safety**: Complete integration of TypedDict definitions from `type_definitions.py`
13+
- `DeviceInfoDict` now used throughout for device information
14+
- `APIVersionResponse` properly returned from version checks
15+
- `StructuredValuesDict` correctly typed for structured instant values
16+
- `ValueDict` extended with min/max/step fields for number types
17+
- **Home Assistant Compatibility**: Full strict typing compliance for HA integrations
18+
- All public API methods use proper TypedDict types
19+
- MyPy strict mode passes without any type: ignore comments
20+
- PEP-561 compliance maintained and enhanced
21+
- **Code Quality**: Elegant use of string literals for TypedDict keys
22+
- Replaced runtime casting with compile-time type safety
23+
- Improved maintainability and readability
24+
- Better IDE support and autocompletion
25+
26+
### Fixed
27+
28+
- **StaticValues**: Now accepts DeviceInfoDict directly without casting
29+
- **InstantValues**: to_structured_dict() returns proper StructuredValuesDict type
30+
- **Constants**: get_default_device_info() returns correct DeviceInfoDict type
31+
832
## [0.7.7] - 2025-10-27
933

1034
### Added

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,9 @@ Data Classification:
966966
967967
For detailed release notes and version history, please see [CHANGELOG.md](CHANGELOG.md).
968968
969-
### Latest Release (0.7.7)
969+
### Latest Release (0.7.8)
970970
971-
- Added `--print-payload` option for debugging HTTP payloads
971+
- **Enhanced Type Safety**: Full integration of TypedDict definitions from `type_definitions.py`
972+
- **Home Assistant Ready**: Strict typing compliance for Home Assistant integrations
973+
- **Improved API**: DeviceInfoDict, APIVersionResponse, and StructuredValuesDict properly utilized
974+
- **Better Documentation**: Enhanced type annotations and cleaner code structure

src/pooldose/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Async API client for SEKO Pooldose."""
22
from .client import PooldoseClient
33

4-
__version__ = "0.7.7"
4+
__version__ = "0.7.8"
55
__all__ = ["PooldoseClient"]

src/pooldose/client.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44

55
import asyncio
66
import logging
7-
from typing import Optional, Tuple, Any, Dict
7+
from typing import Optional, Tuple, Any
88

99
import aiohttp
1010
from getmac import get_mac_address
1111

1212

1313
from pooldose.constants import get_default_device_info
14+
from pooldose.type_definitions import DeviceInfoDict, StructuredValuesDict, APIVersionResponse
1415
from pooldose.mappings.mapping_info import MappingInfo
1516
from pooldose.request_handler import RequestHandler
1617
from pooldose.request_status import RequestStatus
1718
from pooldose.values.instant_values import InstantValues
1819
from pooldose.values.static_values import StaticValues
1920

20-
# pylint: disable=line-too-long,too-many-instance-attributes
21+
# pylint: disable=line-too-long,too-many-instance-attributes, line-too-long
2122

2223
_LOGGER = logging.getLogger(__name__)
2324

@@ -58,7 +59,7 @@ def __init__(self, host: str, timeout: int = 30, *, websession: Optional[aiohttp
5859
self._request_handler: RequestHandler | None = None
5960

6061
# Initialize device info with default or placeholder values
61-
self.device_info: Dict[str, Any] = get_default_device_info()
62+
self.device_info: DeviceInfoDict = get_default_device_info()
6263

6364
# Mapping-Status und Mapping-Cache
6465
self._mapping_status = None
@@ -103,13 +104,13 @@ def request_handler(self) -> RequestHandler:
103104
raise RuntimeError("Client not connected. Call connect() first.")
104105
return self._request_handler
105106

106-
def check_apiversion_supported(self) -> Tuple[RequestStatus, Dict[str, Optional[str]]]:
107+
def check_apiversion_supported(self) -> Tuple[RequestStatus, APIVersionResponse]:
107108
"""
108109
Check if the loaded API version matches the supported version.
109110
110111
Returns:
111-
tuple: (RequestStatus, dict)
112-
- dict contains:
112+
tuple: (RequestStatus, APIVersionResponse)
113+
- APIVersionResponse contains:
113114
"api_version_is": the current API version (or None if not set)
114115
"api_version_should": the expected API version
115116
- RequestStatus.NO_DATA if not set.
@@ -120,7 +121,7 @@ def check_apiversion_supported(self) -> Tuple[RequestStatus, Dict[str, Optional[
120121
"api_version_should": API_VERSION_SUPPORTED,
121122
}
122123

123-
result = {
124+
result: APIVersionResponse = {
124125
"api_version_is": self._request_handler.api_version,
125126
"api_version_should": API_VERSION_SUPPORTED,
126127
}
@@ -233,7 +234,7 @@ def static_values(self) -> tuple[RequestStatus, StaticValues | None]:
233234
tuple: (RequestStatus, StaticValues|None) - Status and static values object.
234235
"""
235236
try:
236-
# Device info is already Dict[str, Any]
237+
# Device info is DeviceInfoDict and StaticValues now accepts it directly
237238
return RequestStatus.SUCCESS, StaticValues(self.device_info)
238239
except (ValueError, TypeError, KeyError) as err:
239240
_LOGGER.warning("Error creating StaticValues: %s", err)
@@ -270,12 +271,12 @@ async def instant_values(self) -> tuple[RequestStatus, InstantValues | None]:
270271
_LOGGER.warning("Error creating InstantValues: %s", err)
271272
return RequestStatus.UNKNOWN_ERROR, None
272273

273-
async def instant_values_structured(self) -> Tuple[RequestStatus, Dict[str, Any]]:
274+
async def instant_values_structured(self) -> Tuple[RequestStatus, StructuredValuesDict]:
274275
"""
275276
Get instant values in structured JSON format with types as top-level keys.
276277
277278
Returns:
278-
Tuple[RequestStatus, Dict[str, Any]]: Status and structured data dict.
279+
Tuple[RequestStatus, StructuredValuesDict]: Status and structured data dict.
279280
"""
280281
# Get instant values object
281282
status, instant_values = await self.instant_values()

src/pooldose/constants.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Constants for the Pooldose library."""
22

3-
from typing import Any, Dict, Optional
3+
from pooldose.type_definitions import DeviceInfoDict
44

55
# Default device info structure
6-
DEFAULT_DEVICE_INFO: Dict[str, Optional[Any]] = {
6+
DEFAULT_DEVICE_INFO: DeviceInfoDict = {
77
"NAME": None, # Device name
88
"SERIAL_NUMBER": None, # Serial number
99
"DEVICE_ID": None, # Device ID, i.e., SERIAL_NUMBER + "_DEVICE"
@@ -24,6 +24,6 @@
2424
}
2525

2626

27-
def get_default_device_info() -> Dict[str, Optional[Any]]:
27+
def get_default_device_info() -> DeviceInfoDict:
2828
"""Return a copy of the default device info structure."""
2929
return DEFAULT_DEVICE_INFO.copy()

src/pooldose/mock_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any, Dict, List, Optional, Tuple, Union
1111

1212
from pooldose.constants import get_default_device_info
13+
from pooldose.type_definitions import StructuredValuesDict
1314
from pooldose.mappings.mapping_info import MappingInfo
1415
from pooldose.request_status import RequestStatus
1516
from pooldose.values.instant_values import InstantValues
@@ -229,7 +230,7 @@ def get_last_payload(self) -> Optional[str]:
229230
return self._last_payload
230231

231232

232-
async def instant_values_structured(self) -> Tuple[RequestStatus, Dict[str, Any]]:
233+
async def instant_values_structured(self) -> Tuple[RequestStatus, StructuredValuesDict]:
233234
"""
234235
Get structured instant values from mock data.
235236

src/pooldose/type_definitions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class ValueDict(TypedDict, total=False):
4444
raw_value: Any
4545
status: str
4646
timestamp: int
47+
min: Any
48+
max: Any
49+
step: Any
4750

4851
# Structured values by type
4952
class StructuredValuesDict(TypedDict, total=False):

src/pooldose/values/instant_values.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import logging
44
from typing import Any, Dict, Tuple, Union
55

6+
from pooldose.type_definitions import StructuredValuesDict
7+
68

79
# pylint: disable=line-too-long,too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-return-statements,too-many-branches,no-else-return,too-many-public-methods
810

@@ -68,12 +70,14 @@ def get(self, key: str, default=None):
6870
except KeyError:
6971
return default
7072

71-
def to_structured_dict(self) -> Dict[str, Any]:
73+
def to_structured_dict(self) -> StructuredValuesDict:
7274
"""
7375
Convert instant values to structured dictionary format with types as top-level keys.
7476
7577
Returns:
76-
Dict[str, Any]: Structured data with format:
78+
StructuredValuesDict: Structured data with format:
79+
{
80+
"sensor": {
7781
"temperature": {"value": 25.5, "unit": "°C"},
7882
"ph": {"value": 7.2, "unit": None}
7983
},
@@ -91,7 +95,7 @@ def to_structured_dict(self) -> Dict[str, Any]:
9195
}
9296
}
9397
"""
94-
structured_data: Dict[str, Dict[str, Any]] = {}
98+
structured_data: StructuredValuesDict = {}
9599

96100
# Process each mapping entry
97101
for mapping_key, mapping_entry in self._mapping.items():
@@ -104,32 +108,50 @@ def to_structured_dict(self) -> Dict[str, Any]:
104108
if raw_entry is None:
105109
continue
106110

107-
# Initialize type section if needed
108-
if entry_type not in structured_data:
109-
structured_data[entry_type] = {}
111+
# Initialize type section if needed - use string literals for TypedDict
112+
if entry_type == "sensor":
113+
if "sensor" not in structured_data:
114+
structured_data["sensor"] = {}
115+
elif entry_type == "switch":
116+
if "switch" not in structured_data:
117+
structured_data["switch"] = {}
118+
elif entry_type == "number":
119+
if "number" not in structured_data:
120+
structured_data["number"] = {}
121+
elif entry_type == "binary_sensor":
122+
if "binary_sensor" not in structured_data:
123+
structured_data["binary_sensor"] = {}
124+
elif entry_type == "select":
125+
if "select" not in structured_data:
126+
structured_data["select"] = {}
110127

111128
# Get the processed value using existing logic
112129
try:
113130
value_data = self._get_value(mapping_key)
114131
if value_data is None:
115132
continue
116133

117-
# Structure the data based on type
134+
# Structure the data based on type - use string literals for TypedDict
118135
if entry_type == "sensor":
119136
if isinstance(value_data, tuple) and len(value_data) >= 2:
120-
structured_data[entry_type][mapping_key] = {
137+
structured_data["sensor"][mapping_key] = {
121138
"value": value_data[0],
122139
"unit": value_data[1]
123140
}
124141

125-
elif entry_type in ("binary_sensor", "switch"):
126-
structured_data[entry_type][mapping_key] = {
142+
elif entry_type == "binary_sensor":
143+
structured_data["binary_sensor"][mapping_key] = {
144+
"value": value_data
145+
}
146+
147+
elif entry_type == "switch":
148+
structured_data["switch"][mapping_key] = {
127149
"value": value_data
128150
}
129151

130152
elif entry_type == "number":
131153
if isinstance(value_data, tuple) and len(value_data) >= 5:
132-
structured_data[entry_type][mapping_key] = {
154+
structured_data["number"][mapping_key] = {
133155
"value": value_data[0],
134156
"unit": value_data[1],
135157
"min": value_data[2],
@@ -138,7 +160,7 @@ def to_structured_dict(self) -> Dict[str, Any]:
138160
}
139161

140162
elif entry_type == "select":
141-
structured_data[entry_type][mapping_key] = {
163+
structured_data["select"][mapping_key] = {
142164
"value": value_data
143165
}
144166

src/pooldose/values/static_values.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Static Values for Async API client for SEKO Pooldose."""
22

33
import logging
4-
from typing import Any, Dict, Optional
4+
from typing import Optional
5+
6+
from pooldose.type_definitions import DeviceInfoDict
57

68
_LOGGER = logging.getLogger(__name__)
79

@@ -14,15 +16,15 @@ class StaticValues:
1416
property with a descriptive name. All properties are read-only.
1517
1618
Args:
17-
device_info (Dict[str, Any]): The dictionary containing static device information.
19+
device_info (DeviceInfoDict): The dictionary containing static device information.
1820
"""
1921

20-
def __init__(self, device_info: Dict[str, Any]):
22+
def __init__(self, device_info: DeviceInfoDict):
2123
"""
2224
Initialize StaticValues.
2325
2426
Args:
25-
device_info (Dict[str, Any]): The dictionary containing static device information.
27+
device_info (DeviceInfoDict): The dictionary containing static device information.
2628
"""
2729
self._device_info = device_info
2830

0 commit comments

Comments
 (0)