Skip to content

feat: enhance weather attribute normalization to support wind gust data across all API endpoints#546

Open
tiimjcb wants to merge 10 commits intojabesq-org:developmentfrom
tiimjcb:feat/normalize-weather-attributes-homestatus
Open

feat: enhance weather attribute normalization to support wind gust data across all API endpoints#546
tiimjcb wants to merge 10 commits intojabesq-org:developmentfrom
tiimjcb:feat/normalize-weather-attributes-homestatus

Conversation

@tiimjcb
Copy link

@tiimjcb tiimjcb commented Nov 29, 2025

Context

For context, I'm working on a PR on the ha-core repository (Home Assistant), on the Netatmo component. The initial issue was that retrieving weather infos with /getstationdata is not responsive enough, since data is aggregated every 10mn. However, the /homestatus endpoint gives almost live data (according to Netatmo).

So I made modifications in ha-core, everything was working except for the gust attributes. The reason? The names of the attributes are different with the /homestatus route :

        "wind_gust": 3,
        "wind_gust_angle": 180

Main changes

  1. Refactored normalize_weather_attributes() :
    I moved from account.py to helpers.py for a better code organization.
    Enhanced it to work recursively with the nested dictionaries and lists

  2. Added support for additional attributes in the mapping
    Added support for the wind_gust attributes, so the mapping works like it should be with the /homestatus endpoint

  3. Applied normalization across all data retrieval paths
    Applied the normalization process to all responses types. For the HOME endpoint, I made sure modules within home data are normalized.

The tests are passing. And of course, it fixes my issue : wind_gust, and wind_gust_angle are retrieved and updated every time I use the /homestatus endpoint.

@cgtobi : I tried to reach you on Discord to have your opinion about the way I'd like to implement this on Home Assistant. Basically, it's just using the homestatus endpoint, but I'd like to directly talk about it with you.

Thanks guys! Have a great day :)

Summary by Sourcery

Normalize Netatmo weather data consistently across responses and extend support for wind gust attributes.

New Features:

  • Support wind gust and wind gust angle attributes in weather data normalization, including homestatus responses.

Enhancements:

  • Move and enhance weather attribute normalization into helpers as a recursive utility applied to nested dictionaries and lists.
  • Apply attribute normalization to all extracted response bodies before further processing, ensuring consistent field names like firmware, signal strengths, and core sensor metrics.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 29, 2025

Reviewer's Guide

Refactors Netatmo weather attribute normalization into a shared recursive helper used at raw-response extraction time, extends the mapping to cover wind gust attributes, and ensures all API response bodies are normalized consistently before ID fixing and home/module construction.

Sequence diagram for normalized Netatmo weather data extraction and device update

sequenceDiagram
    actor HomeAssistant
    participant NetatmoAPI
    participant Helpers_extract_raw_data as Helpers_extract_raw_data
    participant Helpers_normalize as Helpers_normalize_weather_attributes
    participant Helpers_fix_id as Helpers_fix_id
    participant Account_update_devices as Account_update_devices
    participant Home

    HomeAssistant->>NetatmoAPI: request homestatus or getstationsdata
    NetatmoAPI-->>HomeAssistant: resp

    HomeAssistant->>Helpers_extract_raw_data: extract_raw_data(resp, tag)
    Helpers_extract_raw_data->>Helpers_normalize: normalize_weather_attributes(resp.body)
    Helpers_normalize-->>Helpers_extract_raw_data: normalized_body

    alt tag is homes
        Helpers_extract_raw_data->>Helpers_fix_id: fix_id(normalized_body.homes)
        Helpers_fix_id-->>Helpers_extract_raw_data: homes_with_fixed_ids
        Helpers_extract_raw_data-->>HomeAssistant: {homes: homes_with_fixed_ids, errors: normalized_body.errors}
    else tag is devices or modules
        Helpers_extract_raw_data->>Helpers_fix_id: fix_id(normalized_body.tag)
        Helpers_fix_id-->>Helpers_extract_raw_data: raw_data_with_fixed_ids
        Helpers_extract_raw_data-->>HomeAssistant: {tag: raw_data_with_fixed_ids, errors: normalized_body.errors}
    end

    HomeAssistant->>Account_update_devices: update_devices(normalized_and_fixed_data)

    loop for each device and module
        Account_update_devices->>Home: update({home: {modules: [device_data]}})
        Home-->>Account_update_devices: updated_state
    end

    Account_update_devices-->>HomeAssistant: devices_and_homes_updated
Loading

File-Level Changes

Change Details Files
Introduce a shared, recursive weather-attribute normalization helper and centralize the attribute mapping, including wind gust fields.
  • Move ATTRIBUTES_TO_FIX and normalize_weather_attributes from account.py into helpers.py and broaden them to cover additional attributes like wind_gust and wind_gust_angle.
  • Implement recursive normalization that handles dicts and lists, preserves _id, flattens dashboard_data into the parent structure, and backfills id from _id when missing.
  • Ensure non-dict inputs are passed through or normalized element-wise when in a list.
src/pyatmo/helpers.py
Normalize response bodies at extraction time so all downstream consumers see consistent weather attributes and IDs.
  • Call normalize_weather_attributes on resp['body'] inside extract_raw_data and work with the normalized body for all tags.
  • Normalize homes and other tagged payloads before running fix_id, while preserving and reusing any errors field from the normalized body.
  • Return normalized raw data and errors consistently for both homes and non-homes tags.
src/pyatmo/helpers.py
Stop performing per-module normalization in account update flows now that normalization happens earlier in the data pipeline.
  • Remove calls to normalize_weather_attributes when constructing modules_data and device_data within update_devices.
  • Pass through device_data unmodified into Home.update, relying on prior normalization at response extraction time.
  • Delete the now-redundant ATTRIBUTES_TO_FIX and normalize_weather_attributes implementations from account.py.
src/pyatmo/account.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • It looks like modules under the HOME tag are normalized twice (once via normalize_weather_attributes on the full body and again in the HOME-specific modules loop); you can simplify and avoid redundant work by normalizing them only once.
  • The ATTRIBUTES_TO_FIX entry for "_id" is effectively unused because "_id" is handled explicitly in normalize_weather_attributes; consider removing it from the mapping to reduce confusion about how IDs are normalized.
  • Normalizing the entire "body" for the "body" tag path changes the shape of the public data returned by getpublicdata (e.g., key renames there too); please confirm this broader normalization is intentional for that endpoint and not just for homestatus.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- It looks like modules under the HOME tag are normalized twice (once via normalize_weather_attributes on the full body and again in the HOME-specific modules loop); you can simplify and avoid redundant work by normalizing them only once.
- The ATTRIBUTES_TO_FIX entry for "_id" is effectively unused because "_id" is handled explicitly in normalize_weather_attributes; consider removing it from the mapping to reduce confusion about how IDs are normalized.
- Normalizing the entire "body" for the "body" tag path changes the shape of the public data returned by getpublicdata (e.g., key renames there too); please confirm this broader normalization is intentional for that endpoint and not just for homestatus.

## Individual Comments

### Comment 1
<location> `src/pyatmo/helpers.py:39-59` </location>
<code_context>
+
+    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
</code_context>

<issue_to_address>
**suggestion:** The `_id`/`id` handling is slightly redundant and may be simplified to make the intended precedence clearer.

Inside the dict branch, `_id` is handled in two places: once in the loop (copying `_id` and, if `"id" not in raw_data`, setting `id`), and again after the loop (if `has_internal_id` and `"id" not in normalized`, setting `id` from `raw_data["_id"]`). This duplication makes the `id` precedence harder to follow. Consider consolidating this into a single responsibility (e.g., only in the post-loop block, relying on the loop to set `id` when it already exists) so the priority between user-provided `id` and the derived `_id` is explicit.

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

    if isinstance(raw_data, dict):
        normalized: dict[str, Any] = {}
        for key, value in raw_data.items():
            if key == "_id":
                # Preserve the internal identifier as-is; `id` will be derived later
                normalized["_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
            )

        # Derive a public `id` from `_id` only when no explicit `id` was provided
        if "_id" in normalized and "id" not in normalized:
            normalized["id"] = normalized["_id"]

        return normalized
```
</issue_to_address>

### Comment 2
<location> `src/pyatmo/helpers.py:97-101` </location>
<code_context>
         msg = "No device found, errors in response"
         raise NoDeviceError(msg)

+    body = normalize_weather_attributes(resp["body"])
+    if tag == HOME and "modules" in body.get(HOME, {}):
+        body[HOME]["modules"] = [
+            normalize_weather_attributes(module) for module in body[HOME]["modules"]
+        ]
</code_context>

<issue_to_address>
**suggestion:** Modules under `HOME` are normalized twice, which is redundant work and may make future changes harder to reason about.

Because `normalize_weather_attributes` already recurses through nested dicts and lists, normalizing `resp["body"]` will also cover `body[HOME]["modules"]`. The subsequent `if tag == HOME ...` block re-normalizes each module:

```python
body[HOME]["modules"] = [
    normalize_weather_attributes(module) for module in body[HOME]["modules"]
]
```

This is redundant and adds an extra code path to maintain. Consider normalizing either at the top level or within the `modules` block, but only once in a single, clearly defined place.

```suggestion
    body = normalize_weather_attributes(resp["body"])
```
</issue_to_address>

### Comment 3
<location> `src/pyatmo/helpers.py:42-64` </location>
<code_context>
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

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Add guard clause ([`last-if-guard`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/last-if-guard/))
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
- Merge dictionary updates via the union operator ([`dict-assign-update-to-union`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/dict-assign-update-to-union/))

```suggestion
    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"]
    return normalized
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@tiimjcb
Copy link
Author

tiimjcb commented Nov 29, 2025

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The new recursive normalize_weather_attributes now processes the entire resp['body'], including non-weather sections (e.g., errors or other nested metadata); consider constraining recursion to known payload parts (like dashboard_data/modules) to avoid unexpected key renames or structure changes in unrelated data.
  • The special handling of _id in normalize_weather_attributes now preserves both _id and id, whereas the previous mapping effectively exposed only id; please verify whether downstream code expects _id to be absent or that having both keys does not create ambiguity, and update the mapping/logic accordingly if needed.
  • The function signature of normalize_weather_attributes still uses RawData but can now return lists as well as dicts; it may be worth tightening the type alias or adding a short comment about the possible return shapes to make its usage clearer at call sites like extract_raw_data and home.update.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new recursive `normalize_weather_attributes` now processes the entire `resp['body']`, including non-weather sections (e.g., `errors` or other nested metadata); consider constraining recursion to known payload parts (like `dashboard_data`/modules) to avoid unexpected key renames or structure changes in unrelated data.
- The special handling of `_id` in `normalize_weather_attributes` now preserves both `_id` and `id`, whereas the previous mapping effectively exposed only `id`; please verify whether downstream code expects `_id` to be absent or that having both keys does not create ambiguity, and update the mapping/logic accordingly if needed.
- The function signature of `normalize_weather_attributes` still uses `RawData` but can now return lists as well as dicts; it may be worth tightening the type alias or adding a short comment about the possible return shapes to make its usage clearer at call sites like `extract_raw_data` and `home.update`.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@renatoyamane
Copy link

Hi @cgtobi and @jabesq , can we have your review here, please? 😊

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.

Copilot AI review requested due to automatic review settings January 9, 2026 19:02
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors weather attribute normalization in the Netatmo integration to support wind gust data from the /homestatus API endpoint. The main changes move the normalization function to a shared helper module and enhance it to work recursively with nested data structures.

  • Refactored normalize_weather_attributes() from account.py to helpers.py with recursive processing capabilities
  • Added mappings for wind_gust and wind_gust_angle attributes to normalize them to gust_strength and gust_angle
  • Applied normalization universally across all API endpoints through extract_raw_data() and to home module updates

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/pyatmo/helpers.py Adds refactored recursive normalize_weather_attributes() function with expanded attribute mappings and applies it to all responses in extract_raw_data()
src/pyatmo/account.py Removes old normalize_weather_attributes() implementation and imports the new version from helpers
src/pyatmo/home.py Applies normalization to modules during home updates to support /homestatus endpoint data

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +37 to +62
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"]
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.
msg = "No device found, errors in response"
raise NoDeviceError(msg)

body = normalize_weather_attributes(resp["body"])
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.
Comment on lines +37 to +62
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"]
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 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.
Comment on lines +47 to +61
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.
@tiimjcb
Copy link
Author

tiimjcb commented Jan 9, 2026

why did copilot reviewed it I didn't asked for it to do so, I was just syncing my fork with the latest commits 😂

@tiimjcb
Copy link
Author

tiimjcb commented Jan 9, 2026

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • Consider narrowing where normalize_weather_attributes is applied in extract_raw_data, as normalizing the entire body (including non-weather structures like errors) may unintentionally rewrite fields that happen to match keys in ATTRIBUTES_TO_FIX across all response types.
  • When flattening dashboard_data via normalized |= normalize_weather_attributes(value), any key collisions with existing top-level fields in the same object will silently overwrite values; if that’s not desired, it may be safer to keep the dashboard data namespaced or resolve collisions explicitly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider narrowing where `normalize_weather_attributes` is applied in `extract_raw_data`, as normalizing the entire `body` (including non-weather structures like `errors`) may unintentionally rewrite fields that happen to match keys in `ATTRIBUTES_TO_FIX` across all response types.
- When flattening `dashboard_data` via `normalized |= normalize_weather_attributes(value)`, any key collisions with existing top-level fields in the same object will silently overwrite values; if that’s not desired, it may be safer to keep the dashboard data namespaced or resolve collisions explicitly.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@tiimjcb
Copy link
Author

tiimjcb commented Jan 9, 2026

@cgtobi I applied the sourcery-ai suggestions and your feedback on the c6cb1c1 commit. I choose to remove it from the home-module, since it guarantees a certain "consistency" between all the modules by normalizing directly on extract_raw_data. I'm a junior dev, so I may be choosing wrong on that point, but as far as I tested, it works fine and seems logic to me 🤷🏼‍♂️

Also, what do you think about the last sourcery-ai analysis? I haven't seen any "risks" normalizing the whole body based on what the API returns.

Consider narrowing where normalize_weather_attributes is applied in extract_raw_data, as normalizing the entire body (including non-weather structures like errors) may unintentionally rewrite fields that happen to match keys in ATTRIBUTES_TO_FIX across all response types.

@cgtobi
Copy link
Collaborator

cgtobi commented Jan 9, 2026

It was basically my concern as well. But for now I'd say as long as the tests pass it should be okay.

@tiimjcb
Copy link
Author

tiimjcb commented Jan 9, 2026

Great! I don't know if you had time to look at the fix I made on this issue. But while I have your attention on it, I just need to have more infos, especially about the cooldown applied when a call fails.

Also, if this is merged, will it directly be available through a release? That's because I need this fix to be pushed to a latest release, so I can make a PR on ha-core repository.

@cgtobi
Copy link
Collaborator

cgtobi commented Jan 9, 2026

Great! I don't know if you had time to look at the fix I made on this issue. But while I have your attention on it, I just need to have more infos, especially about the cooldown applied when a call fails.

Also, if this is merged, will it directly be available through a release? That's because I need this fix to be pushed to a latest release, so I can make a PR on ha-core repository.

Only once we release a new version. Currently this is not fully automated.

@cgtobi
Copy link
Collaborator

cgtobi commented Jan 10, 2026

@tiimjcb please ensure the typechecker does not complain

@tiimjcb
Copy link
Author

tiimjcb commented Jan 10, 2026

@cgtobi all tests including mypy typecheck are passing locally now 👍🏼

}


def normalize_weather_attributes(raw_data: NormalizableData) -> NormalizableData:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you sure about the return type?

Copy link
Author

Choose a reason for hiding this comment

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

I thought making a return type would satisfy mypy. Otherwise, it would block since the return type wasn't explicitly specified, and "any" would also block it. At least in local.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What type does normalize_weather_attributes return?

Copy link
Author

Choose a reason for hiding this comment

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

From this logic, it returns the same type that it received in input. If we call normalize_weather_attributes with a dict in input, it returns a dict. If it's a list in input, it returns a list.

That's why I declare the type NormalizableData, so mypy is satisfied with the different possibilities.

Copy link
Collaborator

@cgtobi cgtobi Jan 12, 2026

Choose a reason for hiding this comment

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

Take a look at the original function and compare how you changed the types and the internal behaviour.

Copy link
Collaborator

@cgtobi cgtobi Jan 12, 2026

Choose a reason for hiding this comment

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

Suggested change
def normalize_weather_attributes(raw_data: NormalizableData) -> NormalizableData:
T = TypeVar('T')
def normalize_weather_attributes(raw_data: T) -> T:

Also the normalize function needs to be split so that it uses your extended version when calling from within.

normalized["_id"] = value
continue
if key == "dashboard_data" and isinstance(value, dict):
# useless cast here to satisfy mypy
Copy link
Collaborator

Choose a reason for hiding this comment

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

This hints at some type mismatch. A cast should not be necessary.

Copy link
Author

@tiimjcb tiimjcb Jan 12, 2026

Choose a reason for hiding this comment

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

I agree with you that the cast is completly unnecessary. But as said, it's only to satisfy mypy. Without this cast, it blocks. I don't know what else to do in that case 😬

I'll check if there seems to be a sort of mismatch in the types.

Copy link
Author

@tiimjcb tiimjcb Jan 12, 2026

Choose a reason for hiding this comment

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

Checked it again, and I really don't see how I can do it in another way. From what I understand, it seems that mypy expects an explicit type because it needs to guarantee that the passed object (the result of normalize_weather_attributes) is of the expected type (dict). But since that function can return different types, I placed a cast there to explicitly tell mypy that, in this specific context, the type is indeed a dict, even though it's completely unnecessary 🤷🏼‍♂️

msg = "No device found, errors in response"
raise NoDeviceError(msg)

# useless cast here again to satisfy mypy
Copy link
Collaborator

Choose a reason for hiding this comment

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

This hints at some type mismatch. A cast should not be necessary.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants