Skip to content

Commit

Permalink
Add calendar to Husqvarna Automower (home-assistant#120775)
Browse files Browse the repository at this point in the history
* Add Calendar

* update

* change timezone for tests

* fix requirements

* bump aioautomower to 2024.6.3b0

* bump aioautomower to 2024.6.4b0

* fix req

* align dates

* adjust

* nnbw

* better

* improvements

* req

* update requirements

* tests

* tweaks

* shift functions to library

* tests

* bump to aioautomower==2024.9.0b1

* tests

* remove ZoneInfo wrapper

* use timetzone from start_date object

* Update requirements_all.txt

* Fix names in ProgramEvent
  • Loading branch information
Thomas55555 authored Sep 16, 2024
1 parent 089c942 commit fccbaa0
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 11 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/husqvarna_automower/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CALENDAR,
Platform.DEVICE_TRACKER,
Platform.LAWN_MOWER,
Platform.NUMBER,
Expand Down
86 changes: 86 additions & 0 deletions homeassistant/components/husqvarna_automower/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Creates a calendar entity for the mower."""

from datetime import datetime
import logging

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util

from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
entry: AutomowerConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up lawn mower platform."""
coordinator = entry.runtime_data
async_add_entities(
AutomowerCalendarEntity(mower_id, coordinator) for mower_id in coordinator.data
)


class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
"""Representation of the Automower Calendar element."""

_attr_name: str | None = None

def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
) -> None:
"""Set up AutomowerCalendarEntity."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = mower_id
self._event: CalendarEvent | None = None

@property
def event(self) -> CalendarEvent | None:
"""Return the current or next upcoming event."""
schedule = self.mower_attributes.calendar
if schedule.timeline is None:
return None
cursor = schedule.timeline.active_after(dt_util.now())
program_event = next(cursor, None)
_LOGGER.debug("program_event %s", program_event)
if not program_event:
return None
return CalendarEvent(
summary=program_event.schedule_name,
start=program_event.start.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE),
end=program_event.end.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE),
rrule=program_event.rrule_str,
)

async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Return calendar events within a datetime range.
This is only called when opening the calendar in the UI.
"""
schedule = self.mower_attributes.calendar
if schedule.timeline is None:
raise HomeAssistantError("Unable to get events: No schedule set")
cursor = schedule.timeline.overlapping(
start_date,
end_date,
)
return [
CalendarEvent(
summary=program_event.schedule_name,
start=program_event.start.replace(tzinfo=start_date.tzinfo),
end=program_event.end.replace(tzinfo=start_date.tzinfo),
rrule=program_event.rrule_str,
)
for program_event in cursor
]
41 changes: 39 additions & 2 deletions tests/components/husqvarna_automower/fixtures/mower.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"thursday": false,
"friday": true,
"saturday": false,
"sunday": false
"sunday": false,
"workAreaId": 123456
},
{
"start": 0,
Expand All @@ -51,6 +52,42 @@
"thursday": true,
"friday": false,
"saturday": true,
"sunday": false,
"workAreaId": 123456
},
{
"start": 0,
"duration": 480,
"monday": false,
"tuesday": true,
"wednesday": false,
"thursday": true,
"friday": false,
"saturday": true,
"sunday": false,
"workAreaId": 654321
},
{
"start": 60,
"duration": 480,
"monday": true,
"tuesday": true,
"wednesday": false,
"thursday": true,
"friday": false,
"saturday": true,
"sunday": false,
"workAreaId": 654321
},
{
"start": 120,
"duration": 480,
"monday": true,
"tuesday": false,
"wednesday": false,
"thursday": true,
"friday": false,
"saturday": true,
"sunday": false
}
]
Expand All @@ -64,7 +101,7 @@
},
"metadata": {
"connected": true,
"statusTimestamp": 1697669932683
"statusTimestamp": 1685923200000
},
"workAreas": [
{
Expand Down
89 changes: 89 additions & 0 deletions tests/components/husqvarna_automower/snapshots/test_calendar.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# serializer version: 1
# name: test_calendar_snapshot[start_date0-end_date0]
dict({
'calendar.test_mower_1': dict({
'events': list([
dict({
'end': '2023-06-05T09:00:00+02:00',
'start': '2023-06-05T01:00:00+02:00',
'summary': 'Back lawn schedule 2',
}),
dict({
'end': '2023-06-05T10:00:00+02:00',
'start': '2023-06-05T02:00:00+02:00',
'summary': 'Schedule 1',
}),
dict({
'end': '2023-06-06T00:00:00+02:00',
'start': '2023-06-05T19:00:00+02:00',
'summary': 'Front lawn schedule 1',
}),
dict({
'end': '2023-06-06T08:00:00+02:00',
'start': '2023-06-06T00:00:00+02:00',
'summary': 'Back lawn schedule 1',
}),
dict({
'end': '2023-06-06T08:00:00+02:00',
'start': '2023-06-06T00:00:00+02:00',
'summary': 'Front lawn schedule 2',
}),
dict({
'end': '2023-06-06T09:00:00+02:00',
'start': '2023-06-06T01:00:00+02:00',
'summary': 'Back lawn schedule 2',
}),
dict({
'end': '2023-06-08T00:00:00+02:00',
'start': '2023-06-07T19:00:00+02:00',
'summary': 'Front lawn schedule 1',
}),
dict({
'end': '2023-06-08T08:00:00+02:00',
'start': '2023-06-08T00:00:00+02:00',
'summary': 'Back lawn schedule 1',
}),
dict({
'end': '2023-06-08T08:00:00+02:00',
'start': '2023-06-08T00:00:00+02:00',
'summary': 'Front lawn schedule 2',
}),
dict({
'end': '2023-06-08T09:00:00+02:00',
'start': '2023-06-08T01:00:00+02:00',
'summary': 'Back lawn schedule 2',
}),
dict({
'end': '2023-06-08T10:00:00+02:00',
'start': '2023-06-08T02:00:00+02:00',
'summary': 'Schedule 1',
}),
dict({
'end': '2023-06-10T00:00:00+02:00',
'start': '2023-06-09T19:00:00+02:00',
'summary': 'Front lawn schedule 1',
}),
dict({
'end': '2023-06-10T08:00:00+02:00',
'start': '2023-06-10T00:00:00+02:00',
'summary': 'Front lawn schedule 2',
}),
dict({
'end': '2023-06-10T08:00:00+02:00',
'start': '2023-06-10T00:00:00+02:00',
'summary': 'Back lawn schedule 1',
}),
dict({
'end': '2023-06-10T09:00:00+02:00',
'start': '2023-06-10T01:00:00+02:00',
'summary': 'Back lawn schedule 2',
}),
dict({
'end': '2023-06-10T10:00:00+02:00',
'start': '2023-06-10T02:00:00+02:00',
'summary': 'Schedule 1',
}),
]),
}),
})
# ---
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
'thursday': False,
'tuesday': False,
'wednesday': True,
'work_area_id': None,
'work_area_name': None,
'work_area_id': 123456,
'work_area_name': 'Front lawn',
}),
dict({
'duration': 480,
Expand All @@ -29,6 +29,45 @@
'thursday': True,
'tuesday': True,
'wednesday': False,
'work_area_id': 123456,
'work_area_name': 'Front lawn',
}),
dict({
'duration': 480,
'friday': False,
'monday': False,
'saturday': True,
'start': 0,
'sunday': False,
'thursday': True,
'tuesday': True,
'wednesday': False,
'work_area_id': 654321,
'work_area_name': 'Back lawn',
}),
dict({
'duration': 480,
'friday': False,
'monday': True,
'saturday': True,
'start': 60,
'sunday': False,
'thursday': True,
'tuesday': True,
'wednesday': False,
'work_area_id': 654321,
'work_area_name': 'Back lawn',
}),
dict({
'duration': 480,
'friday': False,
'monday': True,
'saturday': True,
'start': 120,
'sunday': False,
'thursday': True,
'tuesday': False,
'wednesday': False,
'work_area_id': None,
'work_area_name': None,
}),
Expand All @@ -43,7 +82,7 @@
}),
'metadata': dict({
'connected': True,
'status_dateteime': '2023-10-18T22:58:52.683000+00:00',
'status_dateteime': '2023-06-05T00:00:00+00:00',
}),
'mower': dict({
'activity': 'PARKED_IN_CS',
Expand Down Expand Up @@ -143,7 +182,7 @@
'auth_implementation': 'husqvarna_automower',
'token': dict({
'access_token': '**REDACTED**',
'expires_at': 1709208000.0,
'expires_at': 1685926800.0,
'expires_in': 86399,
'provider': 'husqvarna',
'refresh_token': '**REDACTED**',
Expand Down
4 changes: 2 additions & 2 deletions tests/components/husqvarna_automower/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)


@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC))
@pytest.mark.freeze_time(datetime.datetime(2023, 6, 5, tzinfo=datetime.UTC))
async def test_button_states_and_commands(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
Expand Down Expand Up @@ -76,7 +76,7 @@ async def test_button_states_and_commands(
mocked_method.assert_called_once_with(TEST_MOWER_ID)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "2024-02-29T11:16:00+00:00"
assert state.state == "2023-06-05T00:16:00+00:00"
getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiException(
"Test error"
)
Expand Down
Loading

0 comments on commit fccbaa0

Please sign in to comment.