Skip to content

Commit

Permalink
fix: Updated default naming of entities to be better for areas of HA …
Browse files Browse the repository at this point in the history
…where space is limited

BREAKING CHANGE:
If you are relying on the current default names, you will need to adjust accordingly. This will not effect the entity ids, so no automations should be effected.
  • Loading branch information
BottlecapDave committed Apr 27, 2024
1 parent eba53bd commit 4f749a3
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 23 deletions.
1 change: 1 addition & 0 deletions custom_components/carbon_intensity/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
EVENT_CURRENT_DAY_RATES = "carbon_intensity_current_day_rates"
EVENT_NEXT_DAY_RATES = "carbon_intensity_next_day_rates"

REFRESH_RATE_IN_MINUTES_RATES = 30
COORDINATOR_REFRESH_IN_SECONDS = 60
24 changes: 20 additions & 4 deletions custom_components/carbon_intensity/coordinators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import logging
from datetime import datetime, timedelta
from typing import Callable, Any

from homeassistant.util.dt import (as_utc)
from ..utils.attributes import dict_to_typed_dict
from ..utils.requests import calculate_next_refresh

_LOGGER = logging.getLogger(__name__)

class BaseCoordinatorResult:
last_retrieved: datetime
next_refresh: datetime
request_attempts: int
refresh_rate_in_minutes: int

def __init__(self, last_retrieved: datetime, request_attempts: int, refresh_rate_in_minutes: int):
self.last_retrieved = last_retrieved
self.request_attempts = request_attempts
self.next_refresh = calculate_next_refresh(last_retrieved, request_attempts, refresh_rate_in_minutes)
_LOGGER.debug(f'last_retrieved: {last_retrieved}; request_attempts: {request_attempts}; refresh_rate_in_minutes: {refresh_rate_in_minutes}; next_refresh: {self.next_refresh}')

def raise_rate_events(now: datetime,
rates: list,
Expand All @@ -26,10 +42,10 @@ def raise_rate_events(now: datetime,
else:
current_rates.append(rate)

event_data = { "rates": current_rates }
event_data = dict_to_typed_dict({ "rates": current_rates })
event_data.update(additional_attributes)
fire_event(current_event_key, dict_to_typed_dict(event_data))
fire_event(current_event_key, event_data)

event_data = { "rates": next_rates }
event_data = dict_to_typed_dict({ "rates": next_rates })
event_data.update(additional_attributes)
fire_event(next_event_key, dict_to_typed_dict(event_data))
fire_event(next_event_key, event_data)
34 changes: 25 additions & 9 deletions custom_components/carbon_intensity/coordinators/rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@
DOMAIN,
EVENT_CURRENT_DAY_RATES,
EVENT_NEXT_DAY_RATES,
REFRESH_RATE_IN_MINUTES_RATES,
)

from ..api_client import CarbonIntensityApiClient
from . import raise_rate_events
from . import BaseCoordinatorResult, raise_rate_events

_LOGGER = logging.getLogger(__name__)

class RatesCoordinatorResult:
class RatesCoordinatorResult(BaseCoordinatorResult):
last_retrieved: datetime
rates: list

def __init__(self, last_retrieved: datetime, rates: list):
self.last_retrieved = last_retrieved
def __init__(self, last_retrieved: datetime, request_attempts: int, rates: list):
super().__init__(last_retrieved, request_attempts, REFRESH_RATE_IN_MINUTES_RATES)
self.rates = rates

async def async_refresh_rates_data(
Expand All @@ -40,15 +41,30 @@ async def async_refresh_rates_data(
period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0))

new_rates: list = None
if ((current.minute % 30) == 0 or
existing_rates_result is None or
existing_rates_result.rates is None or
len(existing_rates_result.rates) < 1 or
if (existing_rates_result is None or
current >= existing_rates_result.next_refresh or
existing_rates_result.rates[-1]["from"] < period_from):
try:
new_rates = await client.async_get_intensity_and_generation_rates(period_from, region)
except:
_LOGGER.debug(f'Failed to retrieve rates for {region}')
if (existing_rates_result is not None):
result = RatesCoordinatorResult(
existing_rates_result.last_retrieved,
existing_rates_result.request_attempts + 1,
existing_rates_result.rates
)
_LOGGER.warning(f"Failed to retrieve new carbon intensity rates - using cached rates. Next attempt at {result.next_refresh}")
else:
# We want to force into our fallback mode
result = RatesCoordinatorResult(
current - timedelta(minutes=REFRESH_RATE_IN_MINUTES_RATES),
2,
None
)
_LOGGER.warning(f"Failed to retrieve new carbon intensity rates. Next attempt at {result.next_refresh}")

return result

if new_rates is not None:
_LOGGER.debug(f'Rates retrieved for {region}')
Expand All @@ -60,7 +76,7 @@ async def async_refresh_rates_data(
EVENT_CURRENT_DAY_RATES,
EVENT_NEXT_DAY_RATES)

return RatesCoordinatorResult(current, new_rates)
return RatesCoordinatorResult(current, 1, new_rates)

return existing_rates_result

Expand Down
24 changes: 17 additions & 7 deletions custom_components/carbon_intensity/entities/current_rating.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging

from homeassistant.core import HomeAssistant, callback
from homeassistant.util.dt import (utcnow)
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity
)
Expand All @@ -10,20 +12,22 @@

from ..entities import get_current_rate
from ..utils import get_region_for_unique_id_from_id, get_region_from_id
from ..utils.attributes import dict_to_typed_dict

_LOGGER = logging.getLogger(__name__)

class CarbonIntensityCurrentRating(CoordinatorEntity, RestoreSensor):
"""Sensor for displaying the current rate."""

def __init__(self, coordinator, region: str):
def __init__(self, hass: HomeAssistant, coordinator, region: str):
"""Init sensor."""
# Pass coordinator to base class
super().__init__(coordinator)
CoordinatorEntity.__init__(self, coordinator)

self._state = None
self._region = region

self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass)

@property
def unique_id(self):
"""The id of the sensor."""
Expand All @@ -32,7 +36,7 @@ def unique_id(self):
@property
def name(self):
"""Name of the sensor."""
return f"Carbon Intensity {get_region_from_id(self._region)} Current Rating"
return f"Current Rating ({get_region_from_id(self._region)})"

@property
def icon(self):
Expand All @@ -52,6 +56,10 @@ def unit_of_measurement(self):
@property
def state(self):
"""The state of the sensor."""
return self._state

@callback
def _handle_coordinator_update(self) -> None:
# Find the current rate. We only need to do this every half an hour
now = utcnow()
self._state = None
Expand All @@ -68,8 +76,9 @@ def state(self):

if current_rate is not None:
self._state = current_rate["intensity_forecast"]

return self._state

self._attributes = dict_to_typed_dict(self._attributes)
super()._handle_coordinator_update()

async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
Expand All @@ -81,8 +90,9 @@ async def async_added_to_hass(self):
self._state = state.state
self._attributes = {}
for x in state.attributes.keys():

if x != "all_rates":
self._attributes[x] = state.attributes[x]

self._attributes = dict_to_typed_dict(self._attributes)

_LOGGER.debug(f'Restored CarbonIntensityCurrentRating state: {self._state}')
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant.components.event import (
EventEntity,
)
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.restore_state import RestoreEntity

from ..const import EVENT_CURRENT_DAY_RATES
Expand All @@ -28,6 +29,7 @@ def __init__(self, hass: HomeAssistant, region):
self._last_updated = None

self._attr_event_types = [EVENT_CURRENT_DAY_RATES]
self.entity_id = generate_entity_id("event.{}", self.unique_id, hass=hass)

@property
def unique_id(self):
Expand All @@ -37,7 +39,7 @@ def unique_id(self):
@property
def name(self):
"""Name of the sensor."""
return f"Carbon Intensity {get_region_from_id(self._region)} Current Day Rates"
return f"Current Day Rates ({get_region_from_id(self._region)})"

@property
def entity_registry_enabled_default(self) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant.components.event import (
EventEntity,
)
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.restore_state import RestoreEntity

from ..const import EVENT_NEXT_DAY_RATES
Expand All @@ -28,6 +29,7 @@ def __init__(self, hass: HomeAssistant, region):
self._last_updated = None

self._attr_event_types = [EVENT_NEXT_DAY_RATES]
self.entity_id = generate_entity_id("event.{}", self.unique_id, hass=hass)

@property
def unique_id(self):
Expand All @@ -37,7 +39,7 @@ def unique_id(self):
@property
def name(self):
"""Name of the sensor."""
return f"Carbon Intensity {get_region_from_id(self._region)} Next Day Rates"
return f"Next Day Rates ({get_region_from_id(self._region)})"

@property
def entity_registry_enabled_default(self) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/carbon_intensity/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ async def async_setup_default_sensors(hass, entry, async_add_entities):

region = config[CONFIG_MAIN_REGION]

entities = [CarbonIntensityCurrentRating(rate_coordinator, region)]
entities = [CarbonIntensityCurrentRating(hass, rate_coordinator, region)]

async_add_entities(entities, True)
15 changes: 15 additions & 0 deletions custom_components/carbon_intensity/utils/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from datetime import datetime, timedelta

def triangle_number(n):
sum = 0
for i in range(1, n + 1):
sum += i * (i + 1) / 2
return sum

def calculate_next_refresh(current: datetime, request_attempts: int, refresh_rate_in_minutes: int):
next_rate = current + timedelta(minutes=refresh_rate_in_minutes)
if (request_attempts > 1):
i = request_attempts - 1
target_minutes = i * (i + 1) / 2
next_rate = next_rate + timedelta(minutes=target_minutes)
return next_rate
26 changes: 26 additions & 0 deletions tests/unit/utils/test_calculate_next_refresh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from datetime import datetime, timedelta
import pytest

from custom_components.carbon_intensity.utils.requests import calculate_next_refresh

@pytest.mark.asyncio
@pytest.mark.parametrize("request_attempts,expected_next_refresh",[
(1, datetime.strptime("2023-07-14T10:35:01+01:00", "%Y-%m-%dT%H:%M:%S%z")),
(2, datetime.strptime("2023-07-14T10:36:01+01:00", "%Y-%m-%dT%H:%M:%S%z")),
(3, datetime.strptime("2023-07-14T10:38:01+01:00", "%Y-%m-%dT%H:%M:%S%z")),
(4, datetime.strptime("2023-07-14T10:41:01+01:00", "%Y-%m-%dT%H:%M:%S%z")),
(5, datetime.strptime("2023-07-14T10:45:01+01:00", "%Y-%m-%dT%H:%M:%S%z")),
(6, datetime.strptime("2023-07-14T10:50:01+01:00", "%Y-%m-%dT%H:%M:%S%z"))
])
async def test_when_data_provided_then_expected_rate_is_returned(request_attempts, expected_next_refresh):
# Arrange
refresh_rate_in_minutes = 5
request_datetime = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z")
current = request_datetime + timedelta(minutes=refresh_rate_in_minutes) + timedelta(minutes=request_attempts - 1)

# Act
next_refresh = calculate_next_refresh(request_datetime, request_attempts, refresh_rate_in_minutes)

# Assert
assert next_refresh == expected_next_refresh
assert current <= next_refresh

0 comments on commit 4f749a3

Please sign in to comment.