Skip to content

Commit

Permalink
Merge pull request #21 from swartjean/issue-19
Browse files Browse the repository at this point in the history
Reworked integration to support EskomSePush API
  • Loading branch information
swartjean authored Oct 11, 2022
2 parents f1d1927 + c5bec8a commit e899dbc
Show file tree
Hide file tree
Showing 13 changed files with 769 additions and 157 deletions.
11 changes: 5 additions & 6 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "ludeeus/container:integration",
"image": "ghcr.io/ludeeus/devcontainer/integration:stable",
"name": "Blueprint integration development",
"context": "..",
"appPort": [
"9123:8123"
],
"postCreateCommand": "container install",
"runArgs": [
"-v",
"${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh"
],
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"tabnine.tabnine-vscode"
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": false,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
Expand Down
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,61 @@
[![maintainer][maintenance-shield]][maintainer]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]

This is a simple component to integrate with the [Eskom Loadshedding API](https://loadshedding.eskom.co.za/LoadShedding) and provide [loadshedding](https://en.wikipedia.org/wiki/South_African_energy_crisis)-related status information.
This component integrates with the [EskomSePush](https://sepush.co.za/) API to provide [loadshedding](https://en.wikipedia.org/wiki/South_African_energy_crisis)-related status information.

This integration exposes a sensor for the current stage of loadshedding.
An EskomSePush API key is required in order to use this integration. Please visit the [EskomSePush website](https://sepush.co.za/) to sign up and view API documentation.

**This component will set up the following platforms.**
**This component will set up the following platforms:**

Platform | Description
-- | --
`sensor` | Show loadshedding status information.
`sensor` | Shows loadshedding status information for various areas.
`calendar` | Shows upcoming loadshedding event and schedule information for your area.

**This component will create the following entities:**

Entity | Description
-- | --
`sensor.loadshedding_api_quota` | The EskomSePush API quota associated with your API key.
`sensor.loadshedding_national_status` | The current national loadshedding stage for Eskom-supplied customers.
`sensor.loadshedding_cape_town_status` | The current loadshedding stage for City of Cape Town customers.
`sensor.loadshedding_local_status` | The current loadshedding stage for your specific area.
`calendar.loadshedding_local_events` | Calendar of upcoming loadshedding events for your specific area.
`calendar.loadshedding_local_schedule` | Calendar containing the full 7-day loadshedding schedule for your specific area.

The component update period defaults to 2 hours in order to avoid excess API quota consumption. This can be edited through the integration configuration, but you are responsible for monitoring your own API usage.

The recommended way to automate actions around loadshedding events is to use calendar triggers. Below is an example of a simple automation to turn off a switch one hour before any loadshedding event in your area:

```yaml
alias: Loadshedding Notification
trigger:
- platform: calendar
event: start
entity_id: calendar.loadshedding_local_events
offset: "-1:0:0"
action:
- service: homeassistant.turn_off
data: {}
target:
entity_id: switch.example_device
mode: queued
```
Note that by installing this integration you are using it at your own risk. Neither the creators of this integration, nor the EskomSePush team, will be held responsible for any inaccuracies or errors in the loadshedding information presented.
## Installation
**Note that an EskomSePush API key is required in order to use this integration**
1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
3. In the `custom_components` directory (folder) create a new folder called `eskom_loadshedding`.
4. Download _all_ the files from the `custom_components/eskom_loadshedding/` directory (folder) in this repository.
5. Place the files you downloaded in the new directory (folder) you created.
6. Restart Home Assistant
7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Eskom Loadshedding Interface"
7. In the HA UI go to "Settings" -> "Devices & Services", then click "+ Add Integration" and search for "Eskom Loadshedding Interface"
8. Complete the initial configuration by entering your EskomSePush API key and selecting your loadshedding zone

## Configuration is done in the UI

Expand Down
20 changes: 13 additions & 7 deletions custom_components/eskom_loadshedding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .eskom_interface import eskom_interface

from .const import (
CONF_SCAN_PERIOD,
Expand All @@ -21,12 +21,13 @@
PLATFORMS,
STARTUP_MESSAGE,
)
from .eskom_interface import EskomInterface

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: Config):
"""Set up this integration using YAML is not supported."""
"""Setting up this integration using YAML is not supported."""
return True


Expand All @@ -40,7 +41,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
seconds=entry.options.get(CONF_SCAN_PERIOD, DEFAULT_SCAN_PERIOD)
)

coordinator = EskomDataUpdateCoordinator(hass, scan_period)
# Fetch the configured API key and area ID and create the client
api_key = entry.data.get("api_key")
area_id = entry.data.get("area_id")
session = async_get_clientsession(hass)
client = EskomInterface(session=session, api_key=api_key, area_id=area_id)

coordinator = EskomDataUpdateCoordinator(hass, scan_period, client)
await coordinator.async_refresh()

if not coordinator.last_update_success:
Expand All @@ -64,18 +71,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
class EskomDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""

def __init__(self, hass, scan_period):
def __init__(self, hass, scan_period, client: EskomInterface):
"""Initialize."""
self.api = eskom_interface()
self.client = client
self.platforms = []

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=scan_period)

async def _async_update_data(self):
"""Update data via library."""
try:
data = await self.api.async_get_data()
return data.get("data", {})
return await self.client.async_get_data()
except Exception as exception:
raise UpdateFailed(exception)

Expand Down
161 changes: 161 additions & 0 deletions custom_components/eskom_loadshedding/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""Sensor platform for Eskom Loadshedding Interface."""
from datetime import datetime, timedelta
import re

from homeassistant.components.calendar import CalendarEntity, CalendarEvent

from .const import (
DOMAIN,
LOCAL_EVENTS_ID,
LOCAL_EVENTS_NAME,
LOCAL_SCHEDULE_ID,
LOCAL_SCHEDULE_NAME,
)
from .entity import EskomEntity


async def async_setup_entry(hass, entry, async_add_devices):
"""Setup calendar platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices(
[
LoadsheddingLocalEventCalendar(
coordinator,
entry,
calendar_id=LOCAL_EVENTS_ID,
friendly_name=LOCAL_EVENTS_NAME,
),
LoadsheddingLocalScheduleCalendar(
coordinator,
entry,
calendar_id=LOCAL_SCHEDULE_ID,
friendly_name=LOCAL_SCHEDULE_NAME,
),
]
)


class LoadsheddingLocalEventCalendar(EskomEntity, CalendarEntity):
"""Loadshedding Local Event Calendar class."""

_attr_has_entity_name = True

def __init__(self, coordinator, config_entry, calendar_id: str, friendly_name: str):
"""Initialize."""
self.calendar_id = calendar_id
self.friendly_name = friendly_name
super().__init__(coordinator, config_entry)

@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self.config_entry.entry_id}-{self.calendar_id}"

@property
def name(self):
"""Return the friendly name of the sensor."""
return self.friendly_name

@property
def event(self):
# Return the next event
events = self.coordinator.data.get("area_information", {}).get("events", {})
if events:
time_format = "%Y-%m-%dT%H:%M:%S%z"
next_event_start = datetime.strptime(events[0]["start"], time_format)
next_event_end = datetime.strptime(events[0]["end"], time_format)
return CalendarEvent(next_event_start, next_event_end, events[0]["note"])

async def async_get_events(
self,
hass,
start_date: datetime,
end_date: datetime,
) -> list[CalendarEvent]:
# Create calendar events from loadshedding events
events = self.coordinator.data.get("area_information", {}).get("events", {})
if events:
time_format = "%Y-%m-%dT%H:%M:%S%z"
return [
CalendarEvent(
start=datetime.strptime(event["start"], time_format),
end=datetime.strptime(event["end"], time_format),
summary=event["note"],
)
for event in events
]
else:
return []


class LoadsheddingLocalScheduleCalendar(EskomEntity, CalendarEntity):
"""Loadshedding Local Schedule Calendar class."""

_attr_has_entity_name = True

def __init__(self, coordinator, config_entry, calendar_id, friendly_name: str):
"""Initialize."""
self.calendar_id = calendar_id
self.friendly_name = friendly_name
super().__init__(coordinator, config_entry)

@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self.config_entry.entry_id}-{self.calendar_id}"

@property
def name(self):
"""Return the friendly name of the sensor."""
return self.friendly_name

@property
def event(self):
# Return the next event
events = self.coordinator.data.get("area_information", {}).get("events", {})
if events:
time_format = "%Y-%m-%dT%H:%M:%S%z"
next_event_start = datetime.strptime(events[0]["start"], time_format)
next_event_end = datetime.strptime(events[0]["end"], time_format)
return CalendarEvent(next_event_start, next_event_end, events[0]["note"])

async def async_get_events(
self,
hass,
start_date: datetime,
end_date: datetime,
) -> list[CalendarEvent]:
# Create calendar events from the loadshedding schedule
schedule = self.coordinator.data.get("area_information", {}).get("schedule", {})
if schedule:
# Iterate over each day in the schedule and create calender events for each slot
time_format = "%Y-%m-%dT%H:%M%z"
calendar_events = []
for day in schedule["days"]:
for n, stage in enumerate(day["stages"]):
for time_range in stage:
# Extract the start and end time from the provided time range
times = re.findall(r"\d\d:\d\d", time_range)

# Create datetimes from the extracted times
start_time = datetime.strptime(
f"{day['date']}T{times[0]}+02:00", time_format
)
end_time = datetime.strptime(
f"{day['date']}T{times[1]}+02:00", time_format
)

# If the end time was earlier than the start time it means that the slot ran into the next day
# i.e. 22:30-00:30
if end_time < start_time:
end_time += timedelta(days=1)

calendar_events.append(
CalendarEvent(
start=start_time, end=end_time, summary=f"Stage {n + 1}"
)
)

return calendar_events
else:
return []
Loading

0 comments on commit e899dbc

Please sign in to comment.