Skip to content

Commit

Permalink
feat(binary-sensor): Added cost attributes to target rate sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Apr 7, 2023
1 parent af07221 commit 98c0db9
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 37 deletions.
56 changes: 53 additions & 3 deletions custom_components/octopus_energy/binary_sensors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,29 +128,67 @@ def is_target_rate_active(current_date: datetime, applicable_rates, offset: str
next_duration_in_hours = 0
total_applicable_rates = len(applicable_rates)

current_average_cost = None
current_min_cost = None
current_max_cost = None

next_average_cost = None
next_min_cost = None
next_max_cost = None

if (total_applicable_rates > 0):

# Work our our rate blocks. This is more for intermittent target rates
applicable_rates.sort(key=__get_valid_to)
applicable_rate_blocks = list()
block_valid_from = applicable_rates[0]["valid_from"]

total_cost = 0
min_cost = None
max_cost = None

for index, rate in enumerate(applicable_rates):
if (index > 0 and applicable_rates[index - 1]["valid_to"] != rate["valid_from"]):
diff = applicable_rates[index - 1]["valid_to"] - block_valid_from
minutes = diff.total_seconds() / 60
applicable_rate_blocks.append({
"valid_from": block_valid_from,
"valid_to": applicable_rates[index - 1]["valid_to"],
"duration_in_hours": diff.total_seconds() / 60 / 60
"duration_in_hours": minutes / 60,
"average_cost": total_cost / (minutes / 30),
"min_cost": min_cost,
"max_cost": max_cost
})

block_valid_from = rate["valid_from"]
total_cost = 0
min_cost = None
max_cost = None

total_cost += rate["value_inc_vat"]
if min_cost is None or min_cost > rate["value_inc_vat"]:
min_cost = rate["value_inc_vat"]

if max_cost is None or max_cost < rate["value_inc_vat"]:
max_cost = rate["value_inc_vat"]

# Make sure our final block is added
total_cost = applicable_rates[-1]["value_inc_vat"]
if min_cost is None or min_cost > applicable_rates[-1]["value_inc_vat"]:
min_cost = applicable_rates[-1]["value_inc_vat"]

if max_cost is None or max_cost < applicable_rates[-1]["value_inc_vat"]:
max_cost = applicable_rates[-1]["value_inc_vat"]

diff = applicable_rates[-1]["valid_to"] - block_valid_from
minutes = diff.total_seconds() / 60
applicable_rate_blocks.append({
"valid_from": block_valid_from,
"valid_to": applicable_rates[-1]["valid_to"],
"duration_in_hours": diff.total_seconds() / 60 / 60
"duration_in_hours": minutes / 60,
"average_cost": total_cost / (minutes / 30),
"min_cost": min_cost,
"max_cost": max_cost
})

# Find out if we're within an active block, or find the next block
Expand All @@ -164,15 +202,27 @@ def is_target_rate_active(current_date: datetime, applicable_rates, offset: str

if current_date >= valid_from and current_date < valid_to:
current_duration_in_hours = rate["duration_in_hours"]
current_average_cost = rate["average_cost"]
current_min_cost = rate["min_cost"]
current_max_cost = rate["max_cost"]
is_active = True
elif current_date < valid_from:
next_time = valid_from
next_duration_in_hours = rate["duration_in_hours"]
next_average_cost = rate["average_cost"]
next_min_cost = rate["min_cost"]
next_max_cost = rate["max_cost"]
break

return {
"is_active": is_active,
"current_duration_in_hours": current_duration_in_hours,
"current_average_cost": current_average_cost,
"current_min_cost": current_min_cost,
"current_max_cost": current_max_cost,
"next_time": next_time,
"next_duration_in_hours": next_duration_in_hours
"next_duration_in_hours": next_duration_in_hours,
"next_average_cost": next_average_cost,
"next_min_cost": next_min_cost,
"next_max_cost": next_max_cost,
}
10 changes: 9 additions & 1 deletion custom_components/octopus_energy/binary_sensors/target_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,17 @@ def is_on(self):

active_result = is_target_rate_active(current_date, self._target_rates, offset)

self._attributes["next_time"] = active_result["next_time"]
self._attributes["next_time"] = active_result["next_time"]

self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"]
self._attributes["current_average_cost"] = f'{active_result["current_average_cost"]}p'
self._attributes["current_min_cost"] = f'{active_result["current_min_cost"]}p'
self._attributes["current_max_cost"] = f'{active_result["current_max_cost"]}p'

self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"]
self._attributes["next_average_cost"] = f'{active_result["next_average_cost"]}p'
self._attributes["next_min_cost"] = f'{active_result["next_min_cost"]}p'
self._attributes["next_max_cost"] = f'{active_result["next_max_cost"]}p'

return active_result["is_active"]

Expand Down
Empty file.
149 changes: 116 additions & 33 deletions tests/unit/binary_sensors/test_is_target_rate_active.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ async def test_when_called_before_rates_then_not_active_returned():
{
"valid_from": datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 10
},
{
"valid_from": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 5
},
{
"valid_from": datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 15
}
]

Expand All @@ -33,73 +36,110 @@ async def test_when_called_before_rates_then_not_active_returned():
# Assert
assert result != None
assert result["is_active"] == False

assert result["current_duration_in_hours"] == 0
assert result["current_average_cost"] == None
assert result["current_min_cost"] == None
assert result["current_max_cost"] == None

assert result["next_time"] == rates[0]["valid_from"]
assert result["next_duration_in_hours"] == 1
assert result["next_average_cost"] == 7.5
assert result["next_min_cost"] == 5
assert result["next_max_cost"] == 10

@pytest.mark.asyncio
async def test_when_called_during_rates_then_active_returned():
@pytest.mark.parametrize("test",[
({
"current_date": datetime.strptime("2022-02-09T10:15:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_current_duration_in_hours": 1.5,
"expected_current_average_cost": 13.333333333333334,
"expected_current_min_cost": 10,
"expected_current_max_cost": 15,
"expected_next_duration_in_hours": 1,
"expected_next_average_cost": 12.5,
"expected_next_min_cost": 5,
"expected_next_max_cost": 20,
}),
({
"current_date": datetime.strptime("2022-02-09T12:35:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": datetime.strptime("2022-02-09T14:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_current_duration_in_hours": 1,
"expected_current_average_cost": 12.5,
"expected_current_min_cost": 5,
"expected_current_max_cost": 20,
"expected_next_duration_in_hours": 0.5,
"expected_next_average_cost": 10,
"expected_next_min_cost": 10,
"expected_next_max_cost": 10,
}),
({
"current_date": datetime.strptime("2022-02-09T14:05:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": None,
"expected_current_duration_in_hours": 0.5,
"expected_current_average_cost": 10,
"expected_current_min_cost": 10,
"expected_current_max_cost": 10,
"expected_next_duration_in_hours": 0,
"expected_next_average_cost": None,
"expected_next_min_cost": None,
"expected_next_max_cost": None,
})
])
async def test_when_called_during_rates_then_active_returned(test):
# Arrange
rates = [
{
"valid_from": datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 10,
},
{
"valid_from": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 15,
},
{
"valid_from": datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 15,
},
{
"valid_from": datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 5,
},
{
"valid_from": datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 20,
},
{
"valid_from": datetime.strptime("2022-02-09T14:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 10,
}
]

tests = [
{
"current_date": datetime.strptime("2022-02-09T10:15:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_current_duration_in_hours": 1.5,
"expected_next_duration_in_hours": 1
},
{
"current_date": datetime.strptime("2022-02-09T12:35:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": datetime.strptime("2022-02-09T14:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_current_duration_in_hours": 1,
"expected_next_duration_in_hours": 0.5
},
{
"current_date": datetime.strptime("2022-02-09T14:05:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"expected_next_time": None,
"expected_current_duration_in_hours": 0.5,
"expected_next_duration_in_hours": 0
}
]
result = is_target_rate_active(
test["current_date"],
rates
)

# Assert
assert result != None
assert result["is_active"] == True

for test in tests:
result = is_target_rate_active(
test["current_date"],
rates
)
assert result["current_duration_in_hours"] == test["expected_current_duration_in_hours"]
assert result["current_min_cost"] == test["expected_current_min_cost"]
assert result["current_max_cost"] == test["expected_current_max_cost"]

# Assert
assert result != None
assert result["is_active"] == True
assert result["current_duration_in_hours"] == test["expected_current_duration_in_hours"]
assert result["next_time"] == test["expected_next_time"]
assert result["next_duration_in_hours"] == test["expected_next_duration_in_hours"]
assert result["next_time"] == test["expected_next_time"]
assert result["next_duration_in_hours"] == test["expected_next_duration_in_hours"]
assert result["next_average_cost"] == test["expected_next_average_cost"]
assert result["next_min_cost"] == test["expected_next_min_cost"]
assert result["next_max_cost"] == test["expected_next_max_cost"]

@pytest.mark.asyncio
async def test_when_called_after_rates_then_not_active_returned():
Expand Down Expand Up @@ -127,6 +167,14 @@ async def test_when_called_after_rates_then_not_active_returned():
assert result["is_active"] == False
assert result["next_time"] == None

assert result["current_average_cost"] == None
assert result["current_min_cost"] == None
assert result["current_max_cost"] == None

assert result["next_average_cost"] == None
assert result["next_min_cost"] == None
assert result["next_max_cost"] == None

@pytest.mark.asyncio
async def test_when_offset_set_then_active_at_correct_current_time():
# Arrange
Expand All @@ -136,14 +184,17 @@ async def test_when_offset_set_then_active_at_correct_current_time():
{
"valid_from": datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 10,
},
{
"valid_from": datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 15,
},
{
"valid_from": datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"valid_to": datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"),
"value_inc_vat": 5,
}
]

Expand All @@ -160,6 +211,14 @@ async def test_when_offset_set_then_active_at_correct_current_time():
assert result["is_active"] == False
assert result["next_time"] == datetime.strptime("2022-02-09T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z")

assert result["current_average_cost"] == None
assert result["current_min_cost"] == None
assert result["current_max_cost"] == None

assert result["next_average_cost"] == 12.5
assert result["next_min_cost"] == 10
assert result["next_max_cost"] == 15

# Check where's within our rates and our offset
for minutes_to_add in range(60):
current_date = rates[0]["valid_from"] - timedelta(hours=1) + timedelta(minutes=minutes_to_add)
Expand All @@ -174,6 +233,14 @@ async def test_when_offset_set_then_active_at_correct_current_time():
assert result["is_active"] == True
assert result["next_time"] is not None

assert result["current_average_cost"] == 12.5
assert result["current_min_cost"] == 10
assert result["current_max_cost"] == 15

assert result["next_average_cost"] == 5
assert result["next_min_cost"] == 5
assert result["next_max_cost"] == 5

# Check when within rate but after offset
current_date = rates[0]["valid_from"] - timedelta(hours=1) + timedelta(minutes=61)

Expand All @@ -187,6 +254,14 @@ async def test_when_offset_set_then_active_at_correct_current_time():
assert result["is_active"] == False
assert result["next_time"] == datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z")

assert result["current_average_cost"] == None
assert result["current_min_cost"] == None
assert result["current_max_cost"] == None

assert result["next_average_cost"] == 5
assert result["next_min_cost"] == 5
assert result["next_max_cost"] == 5

@pytest.mark.asyncio
async def test_when_current_date_is_equal_to_last_end_date_then_not_active():
# Arrange
Expand All @@ -209,4 +284,12 @@ async def test_when_current_date_is_equal_to_last_end_date_then_not_active():

assert result != None
assert result["is_active"] == False
assert result["next_time"] == None
assert result["next_time"] == None

assert result["current_average_cost"] == None
assert result["current_min_cost"] == None
assert result["current_max_cost"] == None

assert result["next_average_cost"] == None
assert result["next_min_cost"] == None
assert result["next_max_cost"] == None

0 comments on commit 98c0db9

Please sign in to comment.