Skip to content
Open
32 changes: 1 addition & 31 deletions src/pyatmo/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
SETSTATE_ENDPOINT,
RawData,
)
from pyatmo.helpers import extract_raw_data
from pyatmo.helpers import extract_raw_data, normalize_weather_attributes
from pyatmo.home import Home
from pyatmo.modules.module import Energy, MeasureInterval, Module

Expand Down Expand Up @@ -293,33 +293,3 @@ def find_home_of_device(self, device_data: dict[str, Any]) -> str | None:
),
None,
)


ATTRIBUTES_TO_FIX: dict[str, str] = {
"_id": "id",
"firmware": "firmware_revision",
"wifi_status": "wifi_strength",
"rf_status": "rf_strength",
"Temperature": "temperature",
"Humidity": "humidity",
"Pressure": "pressure",
"CO2": "co2",
"AbsolutePressure": "absolute_pressure",
"Noise": "noise",
"Rain": "rain",
"WindStrength": "wind_strength",
"WindAngle": "wind_angle",
"GustStrength": "gust_strength",
"GustAngle": "gust_angle",
}


def normalize_weather_attributes(raw_data: RawData) -> dict[str, Any]:
"""Normalize weather attributes."""
result: dict[str, Any] = {}
for attribute, value in raw_data.items():
if attribute == "dashboard_data":
result.update(**normalize_weather_attributes(value))
else:
result[ATTRIBUTES_TO_FIX.get(attribute, attribute)] = value
return result
59 changes: 55 additions & 4 deletions src/pyatmo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,55 @@

LOG: logging.Logger = logging.getLogger(__name__)

ATTRIBUTES_TO_FIX: dict[str, str] = {
"firmware": "firmware_revision",
"firmware_revision": "firmware_revision",
"firmware_name": "firmware_name",
"wifi_status": "wifi_strength",
"rf_status": "rf_strength",
"Temperature": "temperature",
"Humidity": "humidity",
"Pressure": "pressure",
"CO2": "co2",
"AbsolutePressure": "absolute_pressure",
"Noise": "noise",
"Rain": "rain",
"WindStrength": "wind_strength",
"WindAngle": "wind_angle",
"GustStrength": "gust_strength",
"GustAngle": "gust_angle",
"wind_gust": "gust_strength",
"wind_gust_angle": "gust_angle",
}


def normalize_weather_attributes(raw_data: RawData) -> RawData:
"""Normalize weather-related attributes recursively."""

if not isinstance(raw_data, dict):
return (
[normalize_weather_attributes(item) for item in raw_data]
if isinstance(raw_data, list)
else raw_data
)
normalized: dict[str, Any] = {}
has_internal_id = "_id" in raw_data
for key, value in raw_data.items():
if key == "_id":
normalized["_id"] = value
if "id" not in raw_data:
normalized.setdefault("id", value)
continue
if key == "dashboard_data" and isinstance(value, dict):
normalized |= normalize_weather_attributes(value)
continue
normalized[ATTRIBUTES_TO_FIX.get(key, key)] = normalize_weather_attributes(
value
)
if has_internal_id and "id" not in normalized:
normalized["id"] = raw_data["_id"]
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for handling "_id" and "id" is duplicated and confusing. Lines 49-53 handle the case when processing "_id" as a key, setting both "_id" and potentially "id". Then lines 60-61 check again if "id" needs to be set from "_id". This creates redundant checks and makes the code harder to understand. Consider simplifying this logic by handling the "_id" to "id" transformation in a single, clear location.

Copilot uses AI. Check for mistakes.
return normalized
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored normalize_weather_attributes function introduces significant new logic including recursive processing, special handling of _id/id relationships, and list processing. However, there are no direct unit tests for this function. Given that this function is now used across all API endpoints to normalize weather data, adding comprehensive unit tests would help ensure correctness and prevent regressions. Consider adding tests that cover: dictionary normalization, list normalization, dashboard_data flattening, _id to id conversion, and the new wind_gust mappings.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored function introduces two significant behavioral changes from the original implementation: (1) It now recursively processes nested dictionaries and lists, whereas the old version only recursively handled dashboard_data. (2) The handling of "_id" has changed - the old code had "_id": "id" in the mapping which would replace _id with id, but the new code preserves both _id and id keys. These changes could impact existing code that depends on the previous behavior. For example, code at line 280 in account.py accesses module["_id"] which would work with the new behavior but may have issues with fully understanding the data flow. Consider documenting these behavioral changes and ensuring all call sites are compatible with the new behavior.

Copilot uses AI. Check for mistakes.


def fix_id(raw_data: list[RawData | str]) -> list[RawData | str]:
"""Fix known errors in station ids like superfluous spaces."""
Expand Down Expand Up @@ -43,20 +92,22 @@ def extract_raw_data(resp: RawData, tag: str) -> RawData:
msg = "No device found, errors in response"
raise NoDeviceError(msg)

body = normalize_weather_attributes(resp["body"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you normalise attributes here already and then again in the home module?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for reviewing!
As far as I remember, it seems you're right, I made a mistake there ^^

I will remove the second normalization in the home module. I probably made this while testing to make sure it would be normalized in every case, and forgot to remove it. Thanks for noticing! 😄

Do you have others comments on this PR? So I can apply everything in one commit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the home module would be more appropriate to do the normalisation. But I need to check out the branch and look into it locally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay so I wait for your feedback on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By all means feel free to investigate yourself as I can't promise fast feedback.

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying normalization to the entire response body at the extraction level is a significant change in scope. Previously, normalize_weather_attributes was only called selectively in specific parts of the codebase. Now it's applied universally to all data flowing through extract_raw_data. While this ensures consistency across all API endpoints, it also means that code throughout the codebase that wasn't previously receiving normalized data will now get it. This could potentially break assumptions in existing code. Consider verifying that all consumers of extract_raw_data can handle the normalized data format, especially the recursive normalization and the _id/id handling changes.

Copilot uses AI. Check for mistakes.

if tag == "homes":
homes: list[dict[str, Any] | str] = fix_id(resp["body"].get(tag))
homes: list[dict[str, Any] | str] = fix_id(body.get(tag))
if not homes:
LOG.debug("Server response (tag: %s): %s", tag, resp)
msg = "No homes found"
raise NoDeviceError(msg)
return {
tag: homes,
"errors": resp["body"].get("errors", []),
"errors": body.get("errors", []),
}

if not (raw_data := fix_id(resp["body"].get(tag))):
if not (raw_data := fix_id(body.get(tag))):
LOG.debug("Server response (tag: %s): %s", tag, resp)
msg = "No device data available"
raise NoDeviceError(msg)

return {tag: raw_data, "errors": resp["body"].get("errors", [])}
return {tag: raw_data, "errors": body.get("errors", [])}
2 changes: 2 additions & 0 deletions src/pyatmo/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
InvalidStateError,
NoScheduleError,
)
from pyatmo.helpers import normalize_weather_attributes
from pyatmo.person import Person
from pyatmo.room import Room
from pyatmo.schedule import Schedule
Expand Down Expand Up @@ -174,6 +175,7 @@ async def update(

has_an_update = False
for module in data.get("modules", []):
module = normalize_weather_attributes(module)
has_an_update = True
if module["id"] not in self.modules:
self.update_topology({"modules": [module]})
Expand Down
Loading