From 443232b7811ccf3db0587d2ebeb6611665112951 Mon Sep 17 00:00:00 2001 From: yurii <jurkash@gmail.com> Date: Sun, 14 Jul 2024 10:44:02 +0300 Subject: [PATCH] Fix API integration --- .devcontainer.json | 34 ++++++ README.md | 35 +----- custom_components/loe_outages/__init__.py | 4 +- custom_components/loe_outages/api.py | 104 +++++++++++------- custom_components/loe_outages/calendar.py | 25 ++++- custom_components/loe_outages/config_flow.py | 8 +- custom_components/loe_outages/const.py | 9 +- custom_components/loe_outages/coordinator.py | 103 +++++++++-------- custom_components/loe_outages/entity.py | 8 +- custom_components/loe_outages/manifest.json | 10 +- custom_components/loe_outages/models.py | 94 ++++++++++++++++ custom_components/loe_outages/sensor.py | 8 +- .../loe_outages/translations/en.json | 6 +- .../loe_outages/translations/uk.json | 12 +- icons/icon.png | Bin 0 -> 6202 bytes requirements.txt | 2 - 16 files changed, 303 insertions(+), 159 deletions(-) create mode 100644 .devcontainer.json create mode 100644 custom_components/loe_outages/models.py create mode 100644 icons/icon.png diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..0cd0cb1 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "ha-loe-outages", + "image": "mcr.microsoft.com/devcontainers/python:3.12-bullseye", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "tamasfe.even-better-toml" + ], + "settings": { + "python.pythonPath": "/usr/bin/python3", + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" + } + } + } + }, + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/README.md b/README.md index ef4e766..c163303 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -<!-- ![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua/) ---> ![HA LOE Outages Logo](./icons/logo.svg) # ⚡️ HA LOE Outages @@ -36,45 +34,15 @@ If the button doesn't work, follow these steps to add the repository manually: 1. Go to **HACS** → **Integrations** → **...** (top right) → **Custom repositories** 2. Click **Add** -3. Enter `https://github.com/jurakash/ha-loe-outages` in the **URL** field +3. Enter `https://github.com/jurkash/ha-loe-outages` in the **URL** field 4. Choose **Integration** as the **Category** 5. **LOE Outages** will appear in the list of available integrations. Install it as usual. -<!-- -## Usage - -This integration can be configured via the UI. On the **Devices and Services** page, click **Add Integration** and search for **LOE Outages**. - -Find your group by visiting the [LOE][loe] website and entering your address in the search bar. Select your group in the configuration. - -![Configuration flow](https://github.com/jurkash/ha-loe-outages/assets/3459374/e8bfde50-fcbe-45c3-b448-b451b0ac3bcd) - -After configuring, add the integration to your dashboard to view the next planned outages. - -![Device page](https://github.com/jurkash/ha-loe-outages/assets/3459374/df628647-fd2a-455d-9d08-0d1542b67e41) - -The integration also provides a calendar view of planned outages, which can be added to your dashboard via the [Calendar card][calendar-card]. - -![Calendar view](https://github.com/jurkash/ha-loe-outages/assets/3459374/b09c4db3-d0a0-4e06-8dd9-3f4a59f1d63e) - -Here’s an example of a dashboard using this integration: - - -![Dashboard example](https://github.com/jurkash/ha-loe-outages/assets/3459374/26c75595-8984-4a9f-893a-e4b6d838b7f2) --> - -<!-- ## Development - -Interested in contributing to the project? - -First, thank you! Check out the [contributing guideline](./CONTRIBUTING.md) for more information. --> - - ## License MIT © [Yurii Shunkin][jurkash] <!-- Badges --> - [gh-release-url]: https://github.com/jurkash/ha-loe-outages/releases/latest [gh-release-image]: https://img.shields.io/github/v/release/jurkash/ha-loe-outages?style=flat-square [gh-downloads-url]: https://github.com/jurkash/ha-loe-outages/releases @@ -91,7 +59,6 @@ MIT © [Yurii Shunkin][jurkash] [twitter-image]: https://img.shields.io/badge/twitter-%40jurkashok-00ACEE.svg?style=flat-square <!-- References --> - [loe]: https://poweron.loe.lviv.ua/ [home-assistant]: https://www.home-assistant.io/ [jurkash]: https://github.com/jurkash diff --git a/custom_components/loe_outages/__init__.py b/custom_components/loe_outages/__init__.py index a11a18e..774fbe2 100644 --- a/custom_components/loe_outages/__init__.py +++ b/custom_components/loe_outages/__init__.py @@ -1,4 +1,4 @@ -"""Init file for LOE Outages integration.""" +"""Init file for Loe Outages integration.""" from __future__ import annotations @@ -38,4 +38,4 @@ async def async_unload_entry( """Handle removal of an entry.""" LOGGER.info("Unload entry: %s", entry) """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) \ No newline at end of file diff --git a/custom_components/loe_outages/api.py b/custom_components/loe_outages/api.py index d48ef6b..2ddf33c 100644 --- a/custom_components/loe_outages/api.py +++ b/custom_components/loe_outages/api.py @@ -1,56 +1,82 @@ -"""API for LOE outages.""" +"""API for Loe outages.""" -import datetime import logging -import requests - -from .const import API_BASE_URL +import aiohttp +from .models import OutageSchedule +import datetime +import pytz LOGGER = logging.getLogger(__name__) class LoeOutagesApi: - """Class to interact with the API for LOE outages.""" + """Class to interact with API for Loe outages.""" + + schedule: list[OutageSchedule] def __init__(self, group: str) -> None: - """Initialize the LOE OutagesApi.""" + """Initialize the LoeOutagesApi.""" self.group = group - self.api_base_url = API_BASE_URL - - def fetch_schedule(self) -> list[dict]: - """Fetch outages from the API.""" - url = f"{self.api_base_url}/Schedule/latest" - response = requests.get(url) - response.raise_for_status() - data = response.json() - for group in data["groups"]: - if group["id"] == self.group: - return group["intervals"] - return [] - - def get_current_event(self, at: datetime.datetime) -> dict: + self.schedule = [] + + async def async_fetch_json_from_endpoint(self) -> dict: + """Fetch outages from the async API endpoint.""" + url = "https://lps.yuriishunkin.com/api/Schedule/latest" + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + return data + else: + LOGGER.error(f"Failed to fetch schedule: {response.status}") + return None + + async def async_fetch_schedule(self) -> None: + """Fetch outages from the JSON response.""" + schedule_data = await self.async_fetch_json_from_endpoint() + schedule = OutageSchedule.from_dict(schedule_data) + if len(self.schedule) == 0: + self.schedule.append(schedule) + return + + if self.schedule[-1].id != schedule.id: + if self.schedule[-1].dateString == schedule.dateString: + self.schedule.remove(self.schedule[-1]) + self.schedule.append(schedule) + + def get_current_event(self, at: datetime) -> dict: """Get the current event.""" - schedule = self.fetch_schedule() - current_event = None - for event in schedule: - start = datetime.datetime.fromisoformat(event["startTime"]) - end = datetime.datetime.fromisoformat(event["endTime"]) - if start <= at <= end: - current_event = event - break - return current_event + if not self.schedule: + return None + + twoDaysBefore = datetime.datetime.now() + datetime.timedelta(days=-2) + for schedule in reversed(self.schedule): + if schedule.date < twoDaysBefore.astimezone(pytz.UTC): + 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 def get_events( self, start_date: datetime.datetime, end_date: datetime.datetime, ) -> list[dict]: - """Get all events between start_date and end_date.""" - schedule = self.fetch_schedule() - events = [] - for event in schedule: - start = datetime.datetime.fromisoformat(event["startTime"]) - end = datetime.datetime.fromisoformat(event["endTime"]) - if start_date <= start <= end_date or start_date <= end <= end_date: - events.append(event) - return events + """Get all events.""" + if not self.schedule: + return [] + + result = [] + twoDaysBeforeStart = start_date + datetime.timedelta(days=-2) + for schedule in reversed(self.schedule): + if schedule.date < twoDaysBeforeStart: + break + + for interval in schedule.between(self.group, start_date, end_date): + result.append(interval) + + return result + + \ No newline at end of file diff --git a/custom_components/loe_outages/calendar.py b/custom_components/loe_outages/calendar.py index 5182c28..a685fa0 100644 --- a/custom_components/loe_outages/calendar.py +++ b/custom_components/loe_outages/calendar.py @@ -1,4 +1,4 @@ -"""Calendar platform for LOE outages integration.""" +"""Calendar platform for Loe outages integration.""" import datetime import logging @@ -21,7 +21,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LOE outages calendar platform.""" + """Set up the Loe outages calendar platform.""" LOGGER.debug("Setup new entry: %s", config_entry) coordinator: LoeOutagesCoordinator = config_entry.runtime_data async_add_entities([LoeOutagesCalendar(coordinator)]) @@ -52,7 +52,13 @@ def event(self) -> CalendarEvent | None: """Return the current or next upcoming event or None.""" now = dt_utils.now() LOGGER.debug("Getting current event for %s", now) - return self.coordinator.get_event_at(now) + interval = self.coordinator.get_event_at(now) + return CalendarEvent( + summary=interval.state, + start=interval.startTime, + end=interval.endTime, + description=interval.state, + ) async def async_get_events( self, @@ -62,4 +68,15 @@ async def async_get_events( ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" LOGGER.debug('Getting all events between "%s" -> "%s"', start_date, end_date) - return self.coordinator.get_events_between(start_date, end_date) + intervals = self.coordinator.get_events_between(start_date, end_date) + events = [] + + for interval in intervals: + event = CalendarEvent( + summary=interval.state, + start=interval.startTime, + end=interval.endTime, + description=interval.state, + ) + events.append(event) + return events \ No newline at end of file diff --git a/custom_components/loe_outages/config_flow.py b/custom_components/loe_outages/config_flow.py index 6e12dc1..c93caf1 100644 --- a/custom_components/loe_outages/config_flow.py +++ b/custom_components/loe_outages/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for LOE Outages integration.""" +"""Config flow for Loe Outages integration.""" import logging from typing import Any @@ -52,7 +52,7 @@ def build_schema(config_entry: ConfigEntry) -> vol.Schema: class LoeOutagesOptionsFlow(OptionsFlow): - """Handle options flow for LOE Outages.""" + """Handle options flow for Loe Outages.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" @@ -71,7 +71,7 @@ async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowRes class LoeOutagesConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for LOE Outages.""" + """Handle a config flow for Loe Outages.""" VERSION = 1 @@ -90,4 +90,4 @@ async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowRes return self.async_show_form( step_id="user", data_schema=build_schema(config_entry=None), - ) + ) \ No newline at end of file diff --git a/custom_components/loe_outages/const.py b/custom_components/loe_outages/const.py index af434a2..403097f 100644 --- a/custom_components/loe_outages/const.py +++ b/custom_components/loe_outages/const.py @@ -1,9 +1,9 @@ -"""Constants for the LOE Outages integration.""" +"""Constants for the Loe Outages integration.""" from typing import Final DOMAIN: Final = "loe_outages" -NAME: Final = "LOE Outages" +NAME: Final = "Loe Outages" # Configuration option CONF_GROUP: Final = "group" @@ -17,10 +17,7 @@ # Values STATE_ON: Final = "PowerOn" STATE_OFF: Final = "PowerOff" -# Endpoint paths -SCHEDULE_PATH = "https://lps.yuriishunkin.com/api/schedule/latest/{group}" -API_BASE_URL = "https://lps.yuriishunkin.com/api" - # Keys TRANSLATION_KEY_EVENT_OFF: Final = f"component.{DOMAIN}.common.electricity_off" +TRANSLATION_KEY_EVENT_ON: Final = f"component.{DOMAIN}.common.electricity_on" diff --git a/custom_components/loe_outages/coordinator.py b/custom_components/loe_outages/coordinator.py index dda98e2..e03e537 100644 --- a/custom_components/loe_outages/coordinator.py +++ b/custom_components/loe_outages/coordinator.py @@ -1,10 +1,9 @@ -"""Coordinator for LOE outages integration.""" +"""Coordinator for Loe outages integration.""" import datetime import logging -import requests -from homeassistant.components.calendar import CalendarEvent +from .models import Interval from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.translation import async_get_translations @@ -18,14 +17,17 @@ STATE_OFF, STATE_ON, TRANSLATION_KEY_EVENT_OFF, + TRANSLATION_KEY_EVENT_ON, UPDATE_INTERVAL, ) LOGGER = logging.getLogger(__name__) +TIMEFRAME_TO_CHECK = datetime.timedelta(hours=24) + class LoeOutagesCoordinator(DataUpdateCoordinator): - """Class to manage fetching LOE outages data.""" + """Class to manage fetching Loe outages data.""" config_entry: ConfigEntry @@ -51,6 +53,7 @@ def event_name_map(self) -> dict: """Return a mapping of event names to translations.""" return { STATE_OFF: self.translations.get(TRANSLATION_KEY_EVENT_OFF), + STATE_ON: self.translations.get(TRANSLATION_KEY_EVENT_ON) } async def update_config( @@ -69,13 +72,13 @@ async def update_config( LOGGER.debug("No group update necessary.") async def _async_update_data(self) -> None: - """Fetch data from the API.""" + """Fetch data from API.""" try: - # Fetch the schedule data from the API - return await self.hass.async_add_executor_job(self.api.fetch_schedule) - except requests.RequestException as err: - LOGGER.exception("Error fetching data for group %s", self.group) - msg = f"Error fetching data from API: {err}" + await self.async_fetch_translations() + return await self.api.async_fetch_schedule() + except FileNotFoundError as err: + LOGGER.exception("Cannot read file for group %s", self.group) + msg = f"File not found: {err}" raise UpdateFailed(msg) from err async def async_fetch_translations(self) -> None: @@ -89,14 +92,30 @@ async def async_fetch_translations(self) -> None: ) LOGGER.debug("Translations loaded: %s", self.translations) + def _get_next_event_of_type(self, state_type: str) -> Interval | None: + """Get the next event of a specific type.""" + now = dt_utils.now() + # Sort events to handle multi-day spanning events correctly + next_events = sorted( + self.get_events_between( + now, + now + TIMEFRAME_TO_CHECK, + translate=False, + ), + key=lambda event: event.startTime, + ) + LOGGER.debug("Next events: %s", next_events) + for event in next_events: + if self._event_to_state(event) == state_type and event.startTime > now: + return event + return None + @property def next_outage(self) -> datetime.datetime | None: """Get the next outage time.""" - next_events = self.get_next_events() - for event in next_events: - if self._event_to_state(event) == STATE_OFF: - return event.start - return None + event = self._get_next_event_of_type(STATE_OFF) + LOGGER.debug("Next outage: %s", event) + return event.startTime if event else None @property def next_connectivity(self) -> datetime.datetime | None: @@ -105,14 +124,12 @@ def next_connectivity(self) -> datetime.datetime | None: current_event = self.get_event_at(now) # If current event is OFF, return the end time if self._event_to_state(current_event) == STATE_OFF: - return current_event.end + return current_event.endTime - # Otherwise, return the next OFF event's end - next_events = self.get_next_events() - for event in next_events: - if self._event_to_state(event) == STATE_OFF: - return event.end - return None + # 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: @@ -121,7 +138,7 @@ def current_state(self) -> str: event = self.get_event_at(now) return self._event_to_state(event) - def get_event_at(self, at: datetime.datetime) -> CalendarEvent: + def get_event_at(self, at: datetime.datetime) -> Interval: """Get the current event.""" event = self.api.get_current_event(at) return self._get_calendar_event(event, translate=False) @@ -132,37 +149,27 @@ def get_events_between( end_date: datetime.datetime, *, translate: bool = True, - ) -> list[CalendarEvent]: + ) -> list[Interval]: """Get all events.""" events = self.api.get_events(start_date, end_date) return [ self._get_calendar_event(event, translate=translate) for event in events ] - def get_next_events(self) -> CalendarEvent: - """Get the next event of a specific type.""" - now = dt_utils.now() - current_event = self.get_event_at(now) - start = current_event.end if current_event else now - end = start + datetime.timedelta(days=1) - return self.get_events_between(start, end, translate=False) - def _get_calendar_event( self, event: dict | None, *, translate: bool = True, - ) -> CalendarEvent: - """Transform an event into a CalendarEvent.""" + ) -> Interval: + """Transform an event into a Inteval.""" if not event: return None - event_summary = event.get("state") - translated_summary = ( - self.event_name_map.get(event_summary) if translate else event_summary - ) - event_start = datetime.datetime.fromisoformat(event["startTime"]) - event_end = datetime.datetime.fromisoformat(event["endTime"]) + event_summary = event['state'] + translated_summary = self.event_name_map.get(event_summary) + event_start = event['startTime'] + event_end = event['endTime'] LOGGER.debug( "Transforming event: %s (%s -> %s)", @@ -171,16 +178,16 @@ def _get_calendar_event( event_end, ) - return CalendarEvent( - summary=translated_summary, - start=event_start, - end=event_end, - description=event_summary, + return Interval( + state=translated_summary if translate else event_summary, + startTime=event_start, + endTime=event_end, ) - def _event_to_state(self, event: CalendarEvent | None) -> str: - summary = event.as_dict().get("summary") if event else None + def _event_to_state(self, event: Interval | None) -> str: + state = event.state if event else None return { + STATE_ON: STATE_ON, STATE_OFF: STATE_OFF, None: STATE_ON, - }[summary] + }[state] \ No newline at end of file diff --git a/custom_components/loe_outages/entity.py b/custom_components/loe_outages/entity.py index cef442b..9719140 100644 --- a/custom_components/loe_outages/entity.py +++ b/custom_components/loe_outages/entity.py @@ -1,4 +1,4 @@ -"""LOE Outages entity.""" +"""Loe Outages entity.""" from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -8,7 +8,7 @@ class LoeOutagesEntity(CoordinatorEntity[LoeOutagesCoordinator]): - """Common logic for LOE Outages entity.""" + """Common logic for Loe Outages entity.""" _attr_has_entity_name = True @@ -19,6 +19,6 @@ def device_info(self) -> DeviceInfo: translation_key="loe_outages", translation_placeholders={"group": self.coordinator.group}, identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, - manufacturer="LOE", + manufacturer="Loe", entry_type=DeviceEntryType.SERVICE, - ) + ) \ No newline at end of file diff --git a/custom_components/loe_outages/manifest.json b/custom_components/loe_outages/manifest.json index 85d76cf..84f520c 100644 --- a/custom_components/loe_outages/manifest.json +++ b/custom_components/loe_outages/manifest.json @@ -1,11 +1,15 @@ { "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": ["icalendar>=5.0.12", "recurring_ical_events>=2.2.1"], - "version": "0.0.0-unreleased" + "requirements": [ + + ], + "version": "0.0.3" } \ No newline at end of file diff --git a/custom_components/loe_outages/models.py b/custom_components/loe_outages/models.py new file mode 100644 index 0000000..f111d92 --- /dev/null +++ b/custom_components/loe_outages/models.py @@ -0,0 +1,94 @@ +import datetime +import pytz +from dateutil import parser +from typing import List + +utc=pytz.UTC + +class Interval: + def __init__(self, state: str, startTime: str, endTime: str): + self.state = state + self.startTime = startTime + self.endTime = endTime + + @staticmethod + def from_dict(obj: dict) -> 'Interval': + return Interval( + state=obj.get("state"), + startTime=parser.parse(obj.get("startTime")).astimezone(utc), + endTime=parser.parse(obj.get("endTime")).astimezone(utc), + ) + + def to_dict(self) -> dict: + return { + "state": self.state, + "startTime": self.startTime, + "endTime": self.endTime + } + +class Group: + def __init__(self, id: str, intervals: List[Interval]): + self.id = id + self.intervals = intervals + + @staticmethod + def from_dict(obj: dict) -> 'Group': + intervals = [Interval.from_dict(interval) for interval in obj.get("intervals", [])] + return Group( + id=obj.get("id"), + intervals=intervals + ) + + def to_dict(self) -> dict: + return { + "id": self.id, + "intervals": [interval.to_dict() for interval in self.intervals] + } + +class OutageSchedule: + def __init__(self, id: str, date: str, dateString: str, imageUrl: str, groups: List[Group]): + self.id = id + self.date = date + self.dateString = dateString + self.imageUrl = imageUrl + self.groups = groups + + @staticmethod + def from_dict(obj: dict) -> 'OutageSchedule': + groups = [Group.from_dict(group) for group in obj.get("groups", [])] + return OutageSchedule( + id=obj.get("id"), + date=parser.parse(obj.get("date")).astimezone(utc), + dateString=obj.get("dateString"), + imageUrl=obj.get("imageUrl"), + groups=groups + ) + + def to_dict(self) -> dict: + return { + "id": self.id, + "date": self.date, + "dateString": self.dateString, + "imageUrl": self.imageUrl, + "groups": [group.to_dict() for group in self.groups] + } + + def get_current_event(self, group_id: str, at: datetime.datetime) -> dict: + at = at.astimezone(utc) + for group in self.groups: + if group.id == group_id: + for interval in group.intervals: + if interval.startTime <= at <= interval.endTime: + return interval.to_dict() + return {} + + def between(self, group_id: str, start: datetime.datetime, end: datetime.datetime) -> list[dict]: + start = start.astimezone(utc) + end = end.astimezone(utc) + res = [] + for group in self.groups: + if group.id == group_id: + for interval in group.intervals: + if start <= interval.startTime and interval.endTime <= end: + res.append(interval.to_dict()) + return res diff --git a/custom_components/loe_outages/sensor.py b/custom_components/loe_outages/sensor.py index 0b2cf96..6a7708c 100644 --- a/custom_components/loe_outages/sensor.py +++ b/custom_components/loe_outages/sensor.py @@ -1,4 +1,4 @@ -"""Calendar platform for LOE outages integration.""" +"""Calendar platform for Loe outages integration.""" import logging from collections.abc import Callable @@ -22,7 +22,7 @@ @dataclass(frozen=True, kw_only=True) class LoeOutagesSensorDescription(SensorEntityDescription): - """LOE Outages entity description.""" + """Loe Outages entity description.""" val_func: Callable[[LoeOutagesCoordinator], bool] @@ -64,7 +64,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LOE outages calendar platform.""" + """Set up the Loe outages calendar platform.""" LOGGER.debug("Setup new entry: %s", config_entry) coordinator: LoeOutagesCoordinator = config_entry.runtime_data async_add_entities( @@ -92,4 +92,4 @@ def __init__( @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return self.entity_description.val_func(self.coordinator) + return self.entity_description.val_func(self.coordinator) \ No newline at end of file diff --git a/custom_components/loe_outages/translations/en.json b/custom_components/loe_outages/translations/en.json index 6ea4ea2..732cb50 100644 --- a/custom_components/loe_outages/translations/en.json +++ b/custom_components/loe_outages/translations/en.json @@ -41,7 +41,7 @@ "message": { "name": "Connectivity", "state": { - "off": "Electricity Outage" + "PowerOff": "Electricity Outage" } } } @@ -51,8 +51,8 @@ "electricity": { "name": "Electricity", "state": { - "on": "Connected", - "off": "Outage" + "PowerOn": "Connected", + "PowerOff": "Outage" } }, "next_outage": { diff --git a/custom_components/loe_outages/translations/uk.json b/custom_components/loe_outages/translations/uk.json index 4622acc..6b887af 100644 --- a/custom_components/loe_outages/translations/uk.json +++ b/custom_components/loe_outages/translations/uk.json @@ -17,7 +17,7 @@ "options": { "step": { "init": { - "title": "Опції LOE Відключення", + "title": "Опції ЛОЕ Відключення", "description": "Оберіть іншу групу:", "data": { "group": "Група" @@ -30,7 +30,7 @@ }, "device": { "loe_outages": { - "name": "LOE Група {group}" + "name": "ЛОЕ Група {group}" } }, "entity": { @@ -41,7 +41,7 @@ "message": { "name": "Підключення", "state": { - "off": "Відключення" + "PowerOff": "Відключення" } } } @@ -51,15 +51,15 @@ "electricity": { "name": "Електрика", "state": { - "on": "Заживлено", - "off": "Відключення" + "PowerOn": "Заживлено", + "PowerOff": "Відключення" } }, "next_outage": { "name": "Наступне відключення" }, "next_connectivity": { - "name": "Наступна заживленість" + "name": "Наступне харчування" } } }, diff --git a/icons/icon.png b/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..848a52c11786371a634ec06f33e5cc75d449f01f GIT binary patch literal 6202 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 YDmxe;ZecFA@yp-R40$s5#MPVs0gHBcYybcN literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 1c8966e..210483d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ homeassistant==2024.7.0 -icalendar >= 5.0.12 pip>=21.0,<24.2 pre-commit>=3.7.1 -recurring-ical-events>=2.2.1 ruff==0.5.0 \ No newline at end of file