Skip to content

Commit

Permalink
Add config options to set min event duration
Browse files Browse the repository at this point in the history
  • Loading branch information
wernerhp committed Jan 11, 2023
1 parent b6e146c commit a7fb153
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 136 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Sign-up for a free Luno wallet using [this invite link](http://www.luno.com/invi

### Bitcoin
`3EGnQKKbF6AijqW9unyBuW8YeEscY5wMSE`
<img width="200" alt="image" src="img_9.png">
<img width="200" alt="Bitcoin address: 3EGnQKKbF6AijqW9unyBuW8YeEscY5wMSE" src="img_9.png">


# Manual Install
Expand Down Expand Up @@ -149,6 +149,5 @@ rest_command:
content_type: "application/json; charset=utf-8"
verify_ssl: true
```
- [Load Shedding (Start)](examples/automation3.yaml)
- [Load Shedding (End)](examples/automation4.yaml)
- [Load Shedding (Start/End)](examples/automation3.yaml)
</details>
90 changes: 42 additions & 48 deletions custom_components/load_shedding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
ATTR_STAGE,
ATTR_START_TIME,
CONF_AREAS,
CONF_MIN_EVENT_DURATION,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
MANUFACTURER,
Expand All @@ -61,29 +62,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up LoadShedding as config entry."""
if not hass.data.get(DOMAIN):
hass.data.setdefault(DOMAIN, {})

sepush: SePush = None
if api_key := entry.options.get(CONF_API_KEY):
if api_key := config_entry.options.get(CONF_API_KEY):
sepush: SePush = SePush(token=api_key)
if not sepush:
return False

stage_coordinator = LoadSheddingStageCoordinator(hass, sepush)
stage_coordinator.update_interval = timedelta(
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)

area_coordinator = LoadSheddingAreaCoordinator(
hass, sepush, stage_coordinator=stage_coordinator
)
area_coordinator.update_interval = timedelta(
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)
for conf in entry.options.get(CONF_AREAS, {}).values():
for conf in config_entry.options.get(CONF_AREAS, {}).values():
area = Area(
id=conf.get(CONF_ID),
name=conf.get(CONF_NAME),
Expand All @@ -95,18 +96,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
quota_coordinator = LoadSheddingQuotaCoordinator(hass, sepush)
quota_coordinator.update_interval = timedelta(seconds=QUOTA_UPDATE_INTERVAL)

hass.data[DOMAIN][entry.entry_id] = {
hass.data[DOMAIN][config_entry.entry_id] = {
ATTR_STAGE: stage_coordinator,
ATTR_AREA: area_coordinator,
ATTR_QUOTA: quota_coordinator,
}

entry.async_on_unload(entry.add_update_listener(update_listener))
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))

await stage_coordinator.async_config_entry_first_refresh()
await area_coordinator.async_config_entry_first_refresh()
await quota_coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

return True

Expand All @@ -119,14 +120,14 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unload_ok


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Reload config entry."""
await hass.config_entries.async_reload(entry.entry_id)
await hass.config_entries.async_reload(config_entry.entry_id)


async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Update listener."""
return await hass.config_entries.async_reload(entry.entry_id)
return await hass.config_entries.async_reload(config_entry.entry_id)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
Expand Down Expand Up @@ -288,17 +289,17 @@ async def _async_update_data(self) -> dict:
return self.data

async def async_update_area(self) -> dict:
"""Retrieve schedule data."""
areas_stage_schedules: dict = {}
"""Retrieve area data."""
area_id_data: dict = {}

for area in self.areas:
# Get forecast for area
events = []
try:
esp = await self.hass.async_add_executor_job(self.sepush.area, area.id)
except SePushError as err:
raise UpdateFailed(err) from err

# Get events for area
events = []
for event in esp.get("events", {}):
note = event.get("note")
parts = str(note).split(" ")
Expand All @@ -318,7 +319,6 @@ async def async_update_area(self) -> dict:

# Get schedule for area
stage_schedule = {}
sast = timezone(timedelta(hours=+2), "SAST")
for day in esp.get("schedule", {}).get("days", []):
date = datetime.strptime(day.get("date"), "%Y-%m-%d")
stage_timeslots = day.get("stages", [])
Expand All @@ -328,30 +328,8 @@ async def async_update_area(self) -> dict:
stage_schedule[stage] = []
for timeslot in timeslots:
start_str, end_str = timeslot.strip().split("-")
start = (
datetime.strptime(start_str, "%H:%M")
.replace(
year=date.year,
month=date.month,
day=date.day,
second=0,
microsecond=0,
tzinfo=sast,
)
.astimezone(timezone.utc)
)
end = (
datetime.strptime(end_str, "%H:%M")
.replace(
year=date.year,
month=date.month,
day=date.day,
second=0,
microsecond=0,
tzinfo=sast,
)
.astimezone(timezone.utc)
)
start = utc_dt(date, datetime.strptime(start_str, "%H:%M"))
end = utc_dt(date, datetime.strptime(end_str, "%H:%M"))
if end < start:
end = end + timedelta(days=1)
stage_schedule[stage].append(
Expand All @@ -362,12 +340,12 @@ async def async_update_area(self) -> dict:
}
)

areas_stage_schedules[area.id] = {
area_id_data[area.id] = {
ATTR_EVENTS: events,
ATTR_SCHEDULE: stage_schedule,
}

return areas_stage_schedules
return area_id_data

async def async_area_forecast(self) -> None:
"""Derive area forecast from planned stages and area schedule."""
Expand All @@ -379,8 +357,6 @@ async def async_area_forecast(self) -> None:
eskom_stages = stages.get(eskom, {}).get(ATTR_PLANNED, [])
cape_town_stages = stages.get(cape_town, {}).get(ATTR_PLANNED, [])

now = datetime.now(timezone.utc)

for area_id, data in self.data.items():
stage_schedules = data.get(ATTR_SCHEDULE)

Expand All @@ -402,9 +378,6 @@ async def async_area_forecast(self) -> None:
start_time = timeslot.get(ATTR_START_TIME)
end_time = timeslot.get(ATTR_END_TIME)

# if end_time < now:
# continue

if start_time >= planned_end_time:
continue
if end_time <= planned_start_time:
Expand All @@ -425,6 +398,13 @@ async def async_area_forecast(self) -> None:
if start_time == end_time:
continue

# Minimum event duration
min_event_dur = self.stage_coordinator.config_entry.options.get(
CONF_MIN_EVENT_DURATION, 30
) # minutes
if end_time - start_time < timedelta(minutes=min_event_dur):
continue

forecast.append(
{
ATTR_STAGE: planned_stage,
Expand All @@ -436,6 +416,20 @@ async def async_area_forecast(self) -> None:
data[ATTR_FORECAST] = forecast


def utc_dt(date: datetime, time: datetime) -> datetime:
"""Given a date and time in SAST, this function returns a datetime object in UTC"""
sast = timezone(timedelta(hours=+2), "SAST")

return time.replace(
year=date.year,
month=date.month,
day=date.day,
second=0,
microsecond=0,
tzinfo=sast,
).astimezone(timezone.utc)


class LoadSheddingQuotaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching LoadShedding Quota."""

Expand Down
6 changes: 6 additions & 0 deletions custom_components/load_shedding/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
CONF_ADD_AREA,
CONF_DELETE_AREA,
CONF_MULTI_STAGE_EVENTS,
CONF_MIN_EVENT_DURATION,
CONF_SETUP_API,
)

Expand Down Expand Up @@ -271,6 +272,10 @@ async def async_step_init(
CONF_MULTI_STAGE_EVENTS,
default=self.opts.get(CONF_MULTI_STAGE_EVENTS, False),
): bool,
vol.Optional(
CONF_MIN_EVENT_DURATION,
default=self.opts.get(CONF_MIN_EVENT_DURATION, 30),
): int,
}
)

Expand All @@ -282,6 +287,7 @@ async def async_step_init(
# if user_input.get(CONF_ACTION) == CONF_DELETE_AREA:
# return await self.async_step_delete_area()
self.opts[CONF_MULTI_STAGE_EVENTS] = user_input.get(CONF_MULTI_STAGE_EVENTS)
self.opts[CONF_MIN_EVENT_DURATION] = user_input.get(CONF_MIN_EVENT_DURATION)
return self.async_create_entry(title=NAME, data=self.opts)

return self.async_show_form(
Expand Down
1 change: 1 addition & 0 deletions custom_components/load_shedding/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
CONF_DELETE_AREA = "delete_area"
CONF_SETUP_API = "setup_api"
CONF_MULTI_STAGE_EVENTS = "multi_stage_events"
CONF_MIN_EVENT_DURATION = "min_event_duration"
CONF_API_KEY: Final = "api_key"
CONF_AREA: Final = "area"
CONF_AREAS: Final = "areas"
Expand Down
19 changes: 13 additions & 6 deletions custom_components/load_shedding/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,14 @@ def extra_state_attributes(self) -> dict[str, list, Any]:
data[ATTR_PLANNED].append(planned)

cur_stage = Stage.NO_LOAD_SHEDDING
if planned := data[ATTR_PLANNED]:

planned = []
if ATTR_PLANNED in data:
planned = data[ATTR_PLANNED]
cur_stage = planned[0].get(ATTR_STAGE, Stage.UNKNOWN)

attrs = get_sensor_attrs(data[ATTR_PLANNED], cur_stage)
attrs[ATTR_PLANNED] = data[ATTR_PLANNED]
attrs = get_sensor_attrs(planned, cur_stage)
attrs[ATTR_PLANNED] = planned
attrs[ATTR_LAST_UPDATE] = self.coordinator.last_update
attrs = clean(attrs)

Expand Down Expand Up @@ -238,7 +241,7 @@ def native_value(self) -> StateType:
events = self.data.get(ATTR_FORECAST, [])

if not events:
return self._attr_native_value
return STATE_OFF

now = datetime.now(timezone.utc)

Expand Down Expand Up @@ -285,8 +288,12 @@ def extra_state_attributes(self) -> dict[str, list, Any]:

data[ATTR_FORECAST].append(forecast)

attrs = get_sensor_attrs(data[ATTR_FORECAST])
attrs[ATTR_FORECAST] = data[ATTR_FORECAST]
forecast = []
if ATTR_FORECAST in data:
forecast = data[ATTR_FORECAST]

attrs = get_sensor_attrs(forecast)
attrs[ATTR_FORECAST] = forecast
attrs[ATTR_LAST_UPDATE] = self.coordinator.last_update
attrs = clean(attrs)

Expand Down
4 changes: 3 additions & 1 deletion custom_components/load_shedding/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
"data": {
"add_area": "Add area",
"delete_area": "Remove area",
"setup_api": "Configure API"
"setup_api": "Configure API",
"multi_stage_events": "Multi-stage events",
"min_event_duration": "Min. event duration (mins)"
}
},
"sepush": {
Expand Down
1 change: 1 addition & 0 deletions custom_components/load_shedding/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"data": {
"add_area": "Add area",
"delete_area": "Remove area",
"min_event_duration": "Min. event duration (mins)",
"multi_stage_events": "Multi-stage events",
"setup_api": "Configure API"
},
Expand Down
52 changes: 36 additions & 16 deletions examples/automation1.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
alias: Load Shedding (Stage)
description: ''
description: ""
trigger:
- platform: state
entity_id:
- sensor.load_shedding_stage
condition:
- condition: template
value_template: >-
{{ trigger.from_state.state != 'unavailable' and trigger.to_state.state != 'unavailable' }}
- sensor.load_shedding_stage_eskom
attribute: stage
condition: []
action:
- service: notify.mobile_app_nokia_8_sirocco
data:
title: Load Shedding
message: |-
{% if is_state_attr(trigger.entity_id, "stage", 0) %}
Suspended
{% else %}
{{ states(trigger.entity_id) }}
{% endif %}
enabled: true
- choose:
- conditions:
- condition: or
Expand Down Expand Up @@ -39,17 +47,29 @@ action:
at: input_datetime.wake
continue_on_timeout: false
default: []
- service: notify.mobile_app_nokia_8_sirocco
data:
title: Load Shedding
message: '{{ states.sensor.load_shedding_stage.state }}'
- service: tts.home_assistant_say
data:
entity_id: media_player.assistant_speakers
cache: true
message: >-
{% if is_state("sensor.load_shedding_stage", "No Load Shedding") %} Load
Shedding suspended {% else %} Load Shedding {{
states.sensor.load_shedding_stage.state }} {% endif %}
enabled: false
mode: single
message: |-
Load Shedding {% if is_state_attr(trigger.entity_id, "stage", 0) %}
Suspended
{% else %}
{{ states(trigger.entity_id) }}
{% endif %}
- delay:
hours: 0
minutes: 0
seconds: 5
milliseconds: 0
- if:
- condition: state
entity_id: sensor.load_shedding_area_eskde_14_milnertoncityofcapetownwesterncape
state: "on"
then:
- service: tts.home_assistant_say
data:
message: Load shedding imminent!
entity_id: media_player.assistant_speakers
cache: true
mode: single
Loading

0 comments on commit a7fb153

Please sign in to comment.