Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@wuede
Copy link

@wuede wuede commented Apr 13, 2025

accept a Schedule object as method param

Summary by Sourcery

Improve schedule handling by modifying the async schedule synchronization methods to work directly with Schedule objects

Enhancements:

  • Refactor schedule synchronization methods to accept Schedule objects instead of separate parameters
  • Simplify the process of updating room temperatures and syncing schedules

Tests:

  • Add new test cases for async schedule temperature setting and synchronization
  • Implement tests to verify schedule modification and synchronization workflows

@wuede wuede requested review from cgtobi and jabesq as code owners April 13, 2025 19:11
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Apr 13, 2025

Reviewer's Guide by Sourcery

This pull request modifies the async_sync_schedule method to accept a Schedule object as a parameter instead of a schedule ID and a dictionary. It also adds new tests to verify the functionality of async_sync_schedule and async_set_schedule_temperatures.

Sequence diagram for async_set_schedule_temperatures

sequenceDiagram
    participant Home
    participant Schedule
    participant Zone
    participant Room

    Home->>Home: async_set_schedule_temperatures(zone_id, temps)
    Home->>Schedule: selected_schedule
    loop For each zone in selected_schedule.zones
        Schedule->>Zone: zone
        alt zone.entity_id == zone_id
            loop For each room in zone.rooms
                Zone->>Room: room
                alt room.entity_id in temps
                    Room->>Room: room.therm_setpoint_temperature = temps[room.entity_id]
                end
            end
        end
    end
    Home->>Home: async_sync_schedule(selected_schedule)
Loading

Sequence diagram for async_sync_schedule

sequenceDiagram
    participant Home
    participant Schedule
    Home->>Home: async_sync_schedule(schedule: Schedule)
    Home->>Schedule: schedule.entity_id
    Home->>Schedule: schedule.away_temp
    Home->>Schedule: schedule.hg_temp
    loop For each timetable_entry in schedule.timetable
        Home->>Schedule: timetable_entry.m_offset
        Home->>Schedule: timetable_entry.zone_id
    end
    loop For each zone in schedule.zones
        Home->>Schedule: zone.entity_id
        Home->>Schedule: zone.name
        Home->>Schedule: zone.type
        loop For each room in zone.rooms
            Home->>Schedule: room.entity_id
            Home->>Schedule: room.therm_setpoint_temperature
        end
    end
    Home->>Home: auth.async_post_api_request(...)
Loading

Updated class diagram for Schedule and Home

classDiagram
    class Home {
        +async_set_schedule_temperatures(zone_id: str, temps: dict[str, float])
        +async_sync_schedule(schedule: Schedule)
    }
    class Schedule {
        +entity_id: str
        +away_temp: float
        +hg_temp: float
        +timetable: list[TimetableEntry]
        +zones: list[Zone]
    }
    class TimetableEntry {
        +m_offset: int
        +zone_id: str
    }
    class Zone {
        +entity_id: str
        +name: str
        +type: str
        +rooms: list[Room]
    }
    class Room {
        +entity_id: str
        +therm_setpoint_temperature: float
    }

    Home -- Schedule : uses
    Schedule -- TimetableEntry : contains
    Schedule -- Zone : contains
    Zone -- Room : contains

    note for Home "async_sync_schedule now accepts a Schedule object"
Loading

File-Level Changes

Change Details Files
Modified async_sync_schedule to accept a Schedule object instead of a schedule ID and a dictionary.
  • Updated the method signature of async_sync_schedule to accept a Schedule object.
  • Modified async_set_schedule_temperatures to use the Schedule object when calling async_sync_schedule.
  • Updated async_sync_schedule to extract the schedule ID from the Schedule object.
  • Updated async_sync_schedule to build the request JSON based on the Schedule object.
  • Updated is_valid_schedule to accept a Schedule object.
src/pyatmo/home.py
Added new tests for async_sync_schedule.
  • Added a test case test_async_sync_schedule to verify the functionality of async_sync_schedule with a valid schedule.
  • Added a test case test_async_sync_schedule_invalid_schedule to verify the behavior of async_sync_schedule with an invalid schedule.
  • Added a test case test_async_set_schedule_temperatures to verify the functionality of async_set_schedule_temperatures.
tests/test_home.py
Added fixture files for testing async_sync_schedule.
  • Added sync_schedule_591b54a2764ff4d50d8b5795.json to mock the API response for syncing a schedule.
  • Added sync_schedule_b1b54a2f45795764f59d50d8.json to mock the API response for syncing a schedule.
fixtures/sync_schedule_591b54a2764ff4d50d8b5795.json
fixtures/sync_schedule_b1b54a2f45795764f59d50d8.json

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!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

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 @wuede - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding type hints to the async_set_schedule_temperatures method for better readability.
  • The test test_async_sync_schedule_invalid_schedule is commented out, consider removing it or fixing it.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟡 Testing: 3 issues found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

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.

"""Check set state data."""
return data is not None


Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Revisit the type validation in is_valid_schedule.

Now that the function expects a Schedule instance instead of a raw dict, it may be beneficial to consider if additional type checks or validations should be performed within is_valid_schedule.

Suggested implementation:

def is_valid_schedule(schedule: Schedule) -> bool:
    """Check schedule with enhanced type validations."""
    # Ensure schedule is an instance of Schedule.
    if not isinstance(schedule, Schedule):
        return False

    # Optionally, validate that schedule has a non-empty "name" attribute.
    if not hasattr(schedule, "name") or not schedule.name:
        return False

    # Optionally, validate that schedule has a "json" attribute that is a dict.
    if not hasattr(schedule, "json") or not isinstance(schedule.json, dict):
        return False

    return True

Additional context may require further validations on the Schedule instance. Adjust the attribute checks (like for "name" and "json") to the actual expected structure of the Schedule class.

Comment on lines +76 to +77
with patch(
"pyatmo.auth.AbstractAsyncAuth.async_post_api_request",
AsyncMock(return_value=MockResponse({"status": "ok"}, 200)),
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Suggest adding tests for unsuccessful API calls.

It's important to test how the code handles unsuccessful API calls. Consider adding tests where async_post_api_request raises an exception or returns a non-200 status code. This will help ensure the code is resilient to network issues and API errors.

Suggested implementation:

import json
import pytest
from unittest.mock import AsyncMock, patch
# Assuming that MockResponse and async_home are available in this file or imported from the appropriate location.

@pytest.mark.asyncio
async def test_async_set_schedule_temperatures_unsuccessful_response():
    temps = {"2746182631": 15}
    # Load fixture file
    with open("fixtures/sync_schedule_591b54a2764ff4d50d8b5795.json", encoding="utf-8") as fixture_file:
        json_fixture = json.load(fixture_file)

    # Simulate a non-200 response (e.g., 500 error)
    with patch(
        "pyatmo.auth.AbstractAsyncAuth.async_post_api_request",
        AsyncMock(return_value=MockResponse({"status": "error"}, 500)),
    ) as mock_resp:
        # Depending on implementation, the function might raise an exception or handle the error internally.
        # Here we assume it will raise an exception.
        with pytest.raises(Exception):
            await async_home.async_set_schedule_temperatures(1, temps)

        mock_resp.assert_awaited()

@pytest.mark.asyncio
async def test_async_set_schedule_temperatures_exception():
    temps = {"2746182631": 15}
    with open("fixtures/sync_schedule_591b54a2764ff4d50d8b5795.json", encoding="utf-8") as fixture_file:
        json_fixture = json.load(fixture_file)

    # Simulate an exception being raised by the API call.
    with patch(
        "pyatmo.auth.AbstractAsyncAuth.async_post_api_request",
        AsyncMock(side_effect=Exception("API failure")),
    ) as mock_resp:
        with pytest.raises(Exception):
            await async_home.async_set_schedule_temperatures(1, temps)

        mock_resp.assert_awaited()

Make sure that:

  1. The tests import pytest, AsyncMock, and patch, as well as any required classes and modules (like MockResponse and async_home).
  2. The expected exception in pytest.raises(Exception) matches what your function actually raises; you might need to replace Exception with a more specific exception type if available.
  3. If async_home.async_set_schedule_temperatures handles errors silently without raising exceptions, adjust the tests accordingly to verify error handling (for example, by checking logs or return values).

Comment on lines +138 to +136
async def test_async_sync_schedule_invalid_schedule(async_home):
"""Test syncing an invalid schedule."""
invalid_schedule = {"invalid": "data"}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Uncomment and complete the test_async_sync_schedule_invalid_schedule test.

This test case seems important for verifying the behavior when an invalid schedule is provided. It should be uncommented and completed to ensure the InvalidSchedule exception is raised as expected.

Suggested implementation:

@pytest.mark.asyncio()
async def test_async_sync_schedule_invalid_schedule(async_home):

Make sure that the function now has only one decorator (the one right above the function definition), so the test framework correctly treats it as an async test, and ensure that your test covers the InvalidSchedule exception scenario.

Comment on lines 142 to 144
with (
pytest.raises(InvalidSchedule),
patch(
"pyatmo.home.is_valid_schedule",
return_value=False,
),
):
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Test different types of invalid schedules.

The test_async_sync_schedule_invalid_schedule test currently uses a generic invalid schedule. Consider adding tests with different types of invalid schedules, such as missing keys, incorrect data types, or invalid values, to ensure comprehensive coverage.

Suggested implementation:

import pytest

@pytest.mark.asyncio()
@pytest.mark.parametrize("invalid_schedule", [
    {"invalid": "data"},  # generic invalid schedule
    {},  # missing keys
    {"start_time": "not_a_timestamp", "end_time": "not_a_timestamp"},  # incorrect data types
    {"start_time": None, "end_time": "2020-01-01T00:00:00Z"},  # invalid start_time value
    {"start_time": "2020-01-01T00:00:00Z", "end_time": -5},  # end_time as invalid numeric value
])
async def test_async_sync_schedule_invalid_schedule(async_home, invalid_schedule):
    """Test syncing various invalid schedules."""

Ensure that the new parameterized test cases match expected scenarios for your application. You might also need to import pytest at the top of the file if not imported already.

Comment on lines 369 to 373
for zone in selected_schedule.zones:
if zone.entity_id == zone_id:
for room in zone.rooms:
if room.entity_id in temps:
room.therm_setpoint_temperature = temps[room.entity_id]
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is quite a deep nesting. How about s.th. like this?

        rooms_by_zone_id = {
            zone.entity_id: {room.entity_id: room for room in zone.rooms}
            for zone in selected_schedule.zones
        }
        for room_id, temperature in temps.items():
            if (
                zone_id not in rooms_by_zone_id
                or room_id not in rooms_by_zone_id[zone_id]
            ):
                continue
            room = rooms_by_zone_id[zone_id][room_id]
            room.therm_setpoint_temperature = temperature

Copy link
Member

Choose a reason for hiding this comment

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

Or even

        for zone in (z for z in selected_schedule.zones if z.entity_id == zone_id):
            for room in (r for r in zone.rooms if r.entity_id in temps):
                room.therm_setpoint_temperature = temps[room.entity_id]

@cgtobi
Copy link
Collaborator

cgtobi commented Apr 24, 2025

Thank you @wuede for your contribution.

Comment on lines 369 to 373
for zone in selected_schedule.zones:
if zone.entity_id == zone_id:
for room in zone.rooms:
if room.entity_id in temps:
room.therm_setpoint_temperature = temps[room.entity_id]
Copy link
Member

Choose a reason for hiding this comment

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

Or even

        for zone in (z for z in selected_schedule.zones if z.entity_id == zone_id):
            for room in (r for r in zone.rooms if r.entity_id in temps):
                room.therm_setpoint_temperature = temps[room.entity_id]

@wuede wuede requested a review from jabesq May 15, 2025 18:49
jabesq
jabesq previously approved these changes May 15, 2025
assert async_home.get_away_temp() == 14


@pytest.mark.asyncio
Copy link
Member

Choose a reason for hiding this comment

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

No need for this mark, pytest-asyncio is doing the identification automatically now

@jabesq
Copy link
Member

jabesq commented May 15, 2025

Please rebase on top of development branch

@wuede wuede force-pushed the feature/improve-schedule-handling branch 2 times, most recently from 12f964a to 514f534 Compare May 16, 2025 11:42
@wuede wuede force-pushed the feature/improve-schedule-handling branch from 514f534 to 291af63 Compare May 16, 2025 12:48
@wuede
Copy link
Author

wuede commented May 16, 2025

Please rebase on top of development branch

rebase was done. Additionally I added the update of the schedules in update_topology in home.py. And sorry for the multiple force pushes. I f***ed up the rebase the first time I did it.

@cgtobi
Copy link
Collaborator

cgtobi commented May 16, 2025

@wuede don't worry, happens to all of us. 😁

@jabesq jabesq merged commit f1644bc into jabesq-org:development May 17, 2025
5 checks passed
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.

3 participants