Skip to content
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

Rework calendar feature #15

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/loe_outages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.runtime_data = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(coordinator.update_config))
entry.async_on_unload(entry.add_update_listener(coordinator.async_update_config))
return True


Expand Down
65 changes: 49 additions & 16 deletions custom_components/loe_outages/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import aiohttp
import datetime
import pytz
from .models import OutageSchedule
from .models import OutageSchedule, Interval

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -46,15 +46,15 @@ async def async_fetch_all_json(self) -> dict:
async def async_fetch_schedules(self) -> None:
"""Fetch outages from the JSON response."""
if len(self.schedules) == 0:
LOGGER.debug("Fetching all schedules")
schedules_data = await self.async_fetch_all_json()
schedules = OutageSchedule.from_list(schedules_data)
for schedule in sorted(schedules, key=lambda s: s.date):
self.schedules.append(schedule)
return
return None
else:
LOGGER.debug("Fetching latest schedules")
schedule_data = await self.async_fetch_latest_json()
schedule = OutageSchedule.from_dict(schedule_data)

new_schedule = OutageSchedule.from_dict(schedule_data)
self.schedules = [
item
Expand All @@ -63,38 +63,71 @@ async def async_fetch_schedules(self) -> None:
]
self.schedules.append(new_schedule)
self.schedules.sort(key=lambda item: item.date)
LOGGER.debug("Saved schedules %s", list(map(lambda s: s.date, self.schedules)))
return None

def get_current_event(self, at: datetime) -> dict:
def get_current_event(self, at: datetime.datetime) -> Interval | None:
"""Get the current event."""
if not self.schedules:
if not self.schedules or len(self.schedules) == 0:
LOGGER.debug("No schedules found")
return None

twoDaysBefore = datetime.datetime.now() + datetime.timedelta(days=-2)
at = at.astimezone(pytz.UTC)
twoDaysBefore = (at + datetime.timedelta(days=-2)).astimezone(pytz.UTC)
for schedule in reversed(self.schedules):
if schedule.date < twoDaysBefore.astimezone(pytz.UTC):
LOGGER.debug("Schedule to compare: %s < %s", schedule.date, twoDaysBefore)
if schedule.date < twoDaysBefore:
return None

events_at = schedule.get_current_event(self.group, at)
if not events_at:
return None
return events_at # return only the first event
if events_at is not None:
LOGGER.debug("Some event was found: %s", events_at)
return events_at # return only the first event

LOGGER.debug("No evets at found")
return None

def get_events(
self,
start_date: datetime.datetime,
end_date: datetime.datetime,
) -> list[dict]:
) -> list[Interval]:
"""Get all events."""
if not self.schedules:
if not self.schedules or len(self.schedules) == 0:
return []

start_date = start_date.astimezone(pytz.UTC)
end_date = end_date.astimezone(pytz.UTC)
result = []
twoDaysBeforeStart = start_date + datetime.timedelta(days=-2)
twoDaysBeforeStart = (start_date + datetime.timedelta(days=-2)).astimezone(
pytz.UTC
)
for schedule in reversed(self.schedules):
if schedule.date < twoDaysBeforeStart:
break

for interval in schedule.between(self.group, start_date, end_date):
for interval in schedule.intersect(self.group, start_date, end_date):
result.append(interval)

return result
return self._merge_intervals(sorted(result, key=lambda i: i.startTime))

def _merge_intervals(self, intervals: list[Interval]) -> list[Interval]:
if not intervals:
return []

# Start with the first interval
merged_intervals = [intervals[0]]

for current in intervals[1:]:
last = merged_intervals[-1]
if last.endTime == current.startTime and last.state == current.state:
merged_intervals[-1] = Interval(
startTime=last.startTime, endTime=current.endTime, state=last.state
)
else:
merged_intervals.append(current)
[
LOGGER.debug("merged: from: %s, to: %s", inter.startTime, inter.endTime)
for inter in merged_intervals
]
return merged_intervals
8 changes: 5 additions & 3 deletions custom_components/loe_outages/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import datetime
import logging
import pytz

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_utils

from .coordinator import LoeOutagesCoordinator
from .entity import LoeOutagesEntity
Expand Down Expand Up @@ -50,9 +50,11 @@ def __init__(
@property
def event(self) -> CalendarEvent | None:
"""Return the current or next upcoming event or None."""
now = dt_utils.now()
utc = pytz.UTC
now = datetime.datetime.now().astimezone(utc)
LOGGER.debug("Getting current event for %s", now)
return self.coordinator.get_calendar_at(now)
res = self.coordinator.get_calendar_at(now)
return res

async def async_get_events(
self,
Expand Down
66 changes: 27 additions & 39 deletions custom_components/loe_outages/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def event_name_map(self) -> dict:
STATE_ON: self.translations.get(TRANSLATION_KEY_EVENT_ON),
}

async def update_config(
async def async_update_config(
self,
hass: HomeAssistant, # noqa: ARG002
config_entry: ConfigEntry,
Expand Down Expand Up @@ -96,12 +96,12 @@ async def async_fetch_translations(self) -> None:

def _get_next_event_of_type(self, state_type: str) -> Interval | None:
"""Get the next event of a specific type."""
now = dt_utils.now()
now = dt_utils.now().astimezone(pytz.UTC)
# Sort events to handle multi-day spanning events correctly
next_events = sorted(
self.get_intervals_between(
now,
now + TIMEFRAME_TO_CHECK,
(now + TIMEFRAME_TO_CHECK).astimezone(pytz.UTC),
translate=False,
),
key=lambda event: event.startTime,
Expand All @@ -122,25 +122,18 @@ def next_outage(self) -> datetime.datetime | None:
@property
def next_connectivity(self) -> datetime.datetime | None:
"""Get next connectivity time."""
now = dt_utils.now()
current_event = self.get_interval_at(now)
# If current event is OFF, return the end time
if self._event_to_state(current_event) == STATE_OFF:
return current_event.endTime

# Otherwise, return the next on event's end
event = self._get_next_event_of_type(STATE_ON)
LOGGER.debug("Next connectivity: %s", event)
return event.startTime if event else None

@property
def current_state(self) -> str:
"""Get the current state."""
now = dt_utils.now()
now = dt_utils.now().astimezone(pytz.UTC)
event = self.get_interval_at(now)
return self._event_to_state(event)

def get_interval_at(self, at: datetime.datetime) -> Interval:
def get_interval_at(self, at: datetime.datetime) -> Interval | None:
"""Get the current event."""
event = self.api.get_current_event(at)
return self._get_interval_event(event, translate=False)
Expand All @@ -160,33 +153,31 @@ def get_intervals_between(

def _get_interval_event(
self,
event: dict | None,
interval: Interval | None,
*,
translate: bool = True,
) -> Interval:
"""Transform an event into a Inteval."""
if not event:
if not interval:
return None

event_summary = event["state"]
translated_summary = self.event_name_map.get(event_summary)
event_start = event["startTime"]
event_end = event["endTime"]
interval_summary = interval.state
translated_summary = self.event_name_map.get(interval_summary)

LOGGER.debug(
"Transforming event: %s (%s -> %s)",
event_summary,
event_start,
event_end,
interval_summary,
interval.startTime,
interval.endTime,
)

return Interval(
state=translated_summary if translate else event_summary,
startTime=event_start,
endTime=event_end,
state=translated_summary if translate else interval_summary,
startTime=interval.startTime,
endTime=interval.endTime,
)

def get_calendar_at(self, at: datetime.datetime) -> CalendarEvent:
def get_calendar_at(self, at: datetime.datetime) -> CalendarEvent | None:
"""Get the current event."""
event = self.api.get_current_event(at)
return self._get_calendar_event(event, translate=False)
Expand All @@ -206,32 +197,29 @@ def get_calendar_between(

def _get_calendar_event(
self,
event: dict | None,
interval: Interval | None,
*,
translate: bool = True,
) -> CalendarEvent:
"""Transform an event into a Inteval."""
if not event:
if not interval:
return None

local_tz = pytz.timezone("Europe/Kyiv")
event_summary = event["state"]
translated_summary = self.event_name_map.get(event_summary)
event_start = event["startTime"].astimezone(local_tz)
event_end = event["endTime"].astimezone(local_tz)
interval_summary = interval.state
translated_summary = self.event_name_map.get(interval_summary)

LOGGER.debug(
"Transforming event: %s (%s -> %s)",
event_summary,
event_start,
event_end,
interval_summary,
interval.startTime,
interval.endTime,
)

return CalendarEvent(
summary=translated_summary if translate else event_summary,
start=event_start,
end=event_end,
description=event_summary,
summary=translated_summary if translate else interval_summary,
start=interval.startTime,
end=interval.endTime,
description=interval_summary,
)

def _event_to_state(self, event: Interval | None) -> str:
Expand Down
12 changes: 4 additions & 8 deletions custom_components/loe_outages/manifest.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
{
"domain": "loe_outages",
"name": "LOE Outages",
"codeowners": [
"@jurkash"
],
"codeowners": ["@jurkash"],
"config_flow": true,
"documentation": "https://github.com/jurkash/ha-loe-outages",
"iot_class": "calculated",
"issue_tracker": "https://github.com/jurkash/ha-loe-outages",
"requirements": [

],
"version": "0.0.3"
}
"requirements": [],
"version": "0.1.0"
}
Loading