-
Notifications
You must be signed in to change notification settings - Fork 35
improve schedule handling #520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
improve schedule handling #520
Conversation
Reviewer's Guide by SourceryThis pull request modifies the Sequence diagram for async_set_schedule_temperaturessequenceDiagram
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)
Sequence diagram for async_sync_schedulesequenceDiagram
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(...)
Updated class diagram for Schedule and HomeclassDiagram
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"
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this 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_temperaturesmethod for better readability. - The test
test_async_sync_schedule_invalid_scheduleis 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
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 | ||
|
|
||
|
|
There was a problem hiding this comment.
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 TrueAdditional 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.
| with patch( | ||
| "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", | ||
| AsyncMock(return_value=MockResponse({"status": "ok"}, 200)), |
There was a problem hiding this comment.
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:
- The tests import pytest, AsyncMock, and patch, as well as any required classes and modules (like MockResponse and async_home).
- 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.
- 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).
| async def test_async_sync_schedule_invalid_schedule(async_home): | ||
| """Test syncing an invalid schedule.""" | ||
| invalid_schedule = {"invalid": "data"} |
There was a problem hiding this comment.
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.
| with ( | ||
| pytest.raises(InvalidSchedule), | ||
| patch( | ||
| "pyatmo.home.is_valid_schedule", | ||
| return_value=False, | ||
| ), | ||
| ): |
There was a problem hiding this comment.
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.
src/pyatmo/home.py
Outdated
| 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] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]
|
Thank you @wuede for your contribution. |
src/pyatmo/home.py
Outdated
| 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] |
There was a problem hiding this comment.
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]
tests/test_home.py
Outdated
| assert async_home.get_away_temp() == 14 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio |
There was a problem hiding this comment.
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
|
Please rebase on top of development branch |
12f964a to
514f534
Compare
514f534 to
291af63
Compare
rebase was done. Additionally I added the update of the schedules in |
|
@wuede don't worry, happens to all of us. 😁 |
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:
Tests: