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
67 changes: 62 additions & 5 deletions src/pyatmo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,64 @@
import logging
from typing import TYPE_CHECKING, Any, cast

from pyatmo.const import HOME
from pyatmo.exceptions import NoDeviceError

if TYPE_CHECKING:
from pyatmo.const import RawData

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

ATTRIBUTES_TO_FIX: dict[str, str] = {
"_id": "id",
"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 isinstance(raw_data, dict):
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.update(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"]
return normalized

if isinstance(raw_data, list):
return [normalize_weather_attributes(item) for item in raw_data]

return raw_data


def fix_id(raw_data: list[RawData | str]) -> list[RawData | str]:
"""Fix known errors in station ids like superfluous spaces."""
Expand All @@ -36,27 +87,33 @@ def fix_id(raw_data: list[RawData | str]) -> list[RawData | str]:
def extract_raw_data(resp: RawData, tag: str) -> RawData:
"""Extract raw data from server response."""
if tag == "body":
return {"public": resp["body"], "errors": []}
return {"public": normalize_weather_attributes(resp["body"]), "errors": []}

if resp is None or "body" not in resp or tag not in resp["body"]:
LOG.debug("Server response (tag: %s): %s", tag, resp)
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 == HOME and "modules" in body.get(HOME, {}):
body[HOME]["modules"] = [
normalize_weather_attributes(module) for module in body[HOME]["modules"]
]

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