Skip to content

Commit

Permalink
fix(sensor): Added back m3/kWh calculations for gas sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Dec 18, 2022
1 parent eb49841 commit 820abde
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 55 deletions.
2 changes: 2 additions & 0 deletions custom_components/octopus_energy/api_client.py
Expand Up @@ -60,6 +60,7 @@
mprn
meters(includeInactive: false) {{
serialNumber
consumptionUnits
}}
agreements {{
validFrom
Expand Down Expand Up @@ -153,6 +154,7 @@ async def async_get_account(self, account_id):
"mprn": mp["meterPoint"]["mprn"],
"meters": list(map(lambda m: {
"serial_number": m["serialNumber"],
"consumption_units": m["consumptionUnits"],
}, mp["meterPoint"]["meters"])),
"agreements": list(map(lambda a: {
"valid_from": a["validFrom"],
Expand Down
17 changes: 10 additions & 7 deletions custom_components/octopus_energy/sensor.py
Expand Up @@ -142,8 +142,8 @@ async def async_setup_default_sensors(hass, entry, async_add_entities):
for meter in point["meters"]:
_LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}')
coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"])
entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"]))
entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, gas_tariff_code, point["mprn"], meter["serial_number"]))
entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"]))
entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, gas_tariff_code, point["mprn"], meter["serial_number"], meter["consumption_units"]))
entities.append(OctopusEnergyGasCurrentRate(client, gas_tariff_code, point["mprn"], meter["serial_number"]))
entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, point["mprn"], meter["serial_number"]))
else:
Expand Down Expand Up @@ -445,7 +445,6 @@ async def async_added_to_hass(self):

_LOGGER.debug(f'Restored state: {self._state}')


class OctopusEnergyPreviousAccumulativeElectricityReading(CoordinatorEntity, OctopusEnergyElectricitySensor):
"""Sensor for displaying the previous days accumulative electricity reading."""

Expand Down Expand Up @@ -838,11 +837,12 @@ async def async_added_to_hass(self):
class OctopusEnergyPreviousAccumulativeGasReading(CoordinatorEntity, OctopusEnergyGasSensor):
"""Sensor for displaying the previous days accumulative gas reading."""

def __init__(self, coordinator, mprn, serial_number):
def __init__(self, coordinator, mprn, serial_number, native_consumption_units):
"""Init sensor."""
super().__init__(coordinator)
OctopusEnergyGasSensor.__init__(self, mprn, serial_number)

self._native_consumption_units = native_consumption_units
self._state = None
self._latest_date = None

Expand Down Expand Up @@ -886,7 +886,8 @@ def state(self):
"""Retrieve the previous days accumulative consumption"""
consumption = calculate_gas_consumption(
self.coordinator.data,
self._latest_date
self._latest_date,
self._native_consumption_units
)

if (consumption != None and len(consumption["consumptions"]) > 2):
Expand Down Expand Up @@ -922,13 +923,14 @@ async def async_added_to_hass(self):
class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor):
"""Sensor for displaying the previous days accumulative gas cost."""

def __init__(self, coordinator, client, tariff_code, mprn, serial_number):
def __init__(self, coordinator, client, tariff_code, mprn, serial_number, native_consumption_units):
"""Init sensor."""
super().__init__(coordinator)
OctopusEnergyGasSensor.__init__(self, mprn, serial_number)

self._client = client
self._tariff_code = tariff_code
self._native_consumption_units = native_consumption_units

self._state = None
self._latest_date = None
Expand Down Expand Up @@ -990,7 +992,8 @@ async def async_update(self):
period_to,
{
"tariff_code": self._tariff_code,
}
},
self._native_consumption_units
)

if (consumption_cost != None and len(consumption_cost["charges"]) > 2):
Expand Down
25 changes: 16 additions & 9 deletions custom_components/octopus_energy/sensor_utils.py
Expand Up @@ -119,7 +119,13 @@ def convert_m3_to_kwh(value):
kwh_value = kwh_value * 40.0 # Calorific value
return round(kwh_value / 3.6, 3) # kWh Conversion factor

def calculate_gas_consumption(consumption_data, last_calculated_timestamp):
# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh
def convert_kwh_to_m3(value):
m3_value = value * 3.6 # kWh Conversion factor
m3_value = m3_value / 40 # Calorific value
return round(m3_value / 1.02264, 3) # Volume correction factor

def calculate_gas_consumption(consumption_data, last_calculated_timestamp, consumption_units):
if (consumption_data != None and len(consumption_data) > 0):

sorted_consumption_data = __sort_consumption(consumption_data)
Expand All @@ -135,10 +141,12 @@ def calculate_gas_consumption(consumption_data, last_calculated_timestamp):

current_consumption = consumption["consumption"]

# Despite what the documentation (https://developer.octopus.energy/docs/api/#consumption) states, after a few emails with
# Octopus Energy and personal experience, gas data is always reported in m3
current_consumption_m3 = current_consumption
current_consumption_kwh = convert_m3_to_kwh(current_consumption)
if consumption_units == "m³":
current_consumption_m3 = current_consumption
current_consumption_kwh = convert_m3_to_kwh(current_consumption)
else:
current_consumption_m3 = convert_kwh_to_m3(current_consumption)
current_consumption_kwh = current_consumption

total_m3 = total_m3 + current_consumption_m3
total_kwh = total_kwh + current_consumption_kwh
Expand All @@ -159,7 +167,7 @@ def calculate_gas_consumption(consumption_data, last_calculated_timestamp):
"consumptions": consumption_parts
}

async def async_calculate_gas_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, sensor):
async def async_calculate_gas_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, sensor, consumption_units):
if (consumption_data != None and len(consumption_data) > 0):

sorted_consumption_data = __sort_consumption(consumption_data)
Expand All @@ -177,9 +185,8 @@ async def async_calculate_gas_cost(client: OctopusEnergyApiClient, consumption_d
for consumption in sorted_consumption_data:
value = consumption["consumption"]

# Despite what the documentation (https://developer.octopus.energy/docs/api/#consumption) states, after a few emails with
# Octopus Energy and personal experience, gas data is always reported in m3. So we need to convert to kWh before we calculate the cost
value = convert_m3_to_kwh(value)
if consumption_units == "m³":
value = convert_m3_to_kwh(value)

consumption_from = consumption["interval_start"]
consumption_to = consumption["interval_end"]
Expand Down
19 changes: 14 additions & 5 deletions tests/integration/test_calculate_gas_consumption.py
Expand Up @@ -6,7 +6,11 @@
from custom_components.octopus_energy.api_client import OctopusEnergyApiClient

@pytest.mark.asyncio
async def test_when_calculate_gas_consumption_uses_real_data_then_calculation_returned():
@pytest.mark.parametrize("consumption_units",[
("m³"),
("kWh")
])
async def test_when_calculate_gas_consumption_uses_real_data_then_calculation_returned(consumption_units):
# Arrange
context = get_test_context()
client = OctopusEnergyApiClient(context["api_key"])
Expand Down Expand Up @@ -34,15 +38,20 @@ async def test_when_calculate_gas_consumption_uses_real_data_then_calculation_re
# Act
consumption = calculate_gas_consumption(
consumption_data,
latest_date
latest_date,
consumption_units
)

# Assert
assert consumption != None
assert consumption["last_calculated_timestamp"] == consumption_data[-1]["interval_end"]

assert consumption["total_kwh"] == 63.86
assert consumption["total_m3"] == 5.62

if consumption_units == "m³":
assert consumption["total_kwh"] == 63.86
assert consumption["total_m3"] == 5.62
else:
assert consumption["total_kwh"] == 5.62
assert consumption["total_m3"] == 0.498

assert len(consumption["consumptions"]) == len(consumption_data)

Expand Down
17 changes: 13 additions & 4 deletions tests/integration/test_calculate_gas_cost.py
Expand Up @@ -6,7 +6,11 @@
from custom_components.octopus_energy.api_client import OctopusEnergyApiClient

@pytest.mark.asyncio
async def test_when_calculate_gas_cost_using_real_data_then_calculation_returned():
@pytest.mark.parametrize("consumption_units",[
("m³"),
("kWh")
])
async def test_when_calculate_gas_cost_using_real_data_then_calculation_returned(consumption_units):
# Arrange
context = get_test_context()
client = OctopusEnergyApiClient(context["api_key"])
Expand Down Expand Up @@ -49,16 +53,21 @@ async def test_when_calculate_gas_cost_using_real_data_then_calculation_returned
period_to,
{
"tariff_code": tariff_code
}
},
consumption_units
)

# Assert
assert consumption_cost != None
assert consumption_cost["last_calculated_timestamp"] == consumption_data[-1]["interval_end"]
assert consumption_cost["standing_charge"] == standard_charge_result["value_inc_vat"]

assert consumption_cost["total_without_standing_charge"] == 2.88
assert consumption_cost["total"] == 3.14
if consumption_units == "m³":
assert consumption_cost["total_without_standing_charge"] == 2.88
assert consumption_cost["total"] == 3.14
else:
assert consumption_cost["total_without_standing_charge"] == 0.25
assert consumption_cost["total"] == 0.52

assert len(consumption_cost["charges"]) == 48

Expand Down
67 changes: 49 additions & 18 deletions tests/unit/test_calculate_gas_consumption.py
Expand Up @@ -12,7 +12,8 @@ async def test_when_gas_consumption_is_none_then_no_calculation_is_returned():
# Act
consumption = calculate_gas_consumption(
None,
latest_date
latest_date,
"m³"
)

# Assert
Expand All @@ -26,7 +27,8 @@ async def test_when_gas_consumption_is_empty_then_no_calculation_is_returned():
# Act
consumption = calculate_gas_consumption(
[],
latest_date
latest_date,
"m³"
)

# Assert
Expand All @@ -44,18 +46,21 @@ async def test_when_gas_consumption_is_before_latest_date_then_no_calculation_is
# Act
consumption = calculate_gas_consumption(
consumption_data,
latest_date
latest_date,
"m³"
)

# Assert
assert consumption == None

@pytest.mark.asyncio
@pytest.mark.parametrize("latest_date",[
(datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z")),
(None)
@pytest.mark.parametrize("latest_date,consumption_units",[
(datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "m³"),
(None, "m³"),
(datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "kWh"),
(None, "kWh")
])
async def test_when_gas_consumption_available_then_calculation_returned(latest_date):
async def test_when_gas_consumption_available_then_calculation_returned(latest_date, consumption_units):
# Arrange
period_from = datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z")
period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z")
Expand All @@ -69,15 +74,20 @@ async def test_when_gas_consumption_available_then_calculation_returned(latest_d
# Act
consumption = calculate_gas_consumption(
consumption_data,
latest_date
latest_date,
consumption_units
)

# Assert
assert consumption != None
assert consumption["last_calculated_timestamp"] == consumption_data[-1]["interval_end"]

assert consumption["total_kwh"] == round(48 * 11.363, 3)
assert consumption["total_m3"] == 48 * 1
if consumption_units == "m³":
assert consumption["total_kwh"] == round(48 * 11.363, 3)
assert consumption["total_m3"] == 48 * 1
else:
assert consumption["total_kwh"] == 48 * 1
assert consumption["total_m3"] == round(48 * 0.088, 3)

assert len(consumption["consumptions"]) == len(consumption_data)

Expand All @@ -92,15 +102,25 @@ async def test_when_gas_consumption_available_then_calculation_returned(latest_d
assert item["to"] == expected_valid_to

assert "consumption_m3" in item
assert item["consumption_m3"] == 1
if consumption_units == "m³":
assert item["consumption_m3"] == 1
else:
assert item["consumption_m3"] == 0.088

assert "consumption_kwh" in item
assert item["consumption_kwh"] == 11.363
if consumption_units == "m³":
assert item["consumption_kwh"] == 11.363
else:
assert item["consumption_kwh"] == 1

expected_valid_from = expected_valid_to

@pytest.mark.asyncio
async def test_when_gas_consumption_starting_at_latest_date_then_calculation_returned():
@pytest.mark.parametrize("consumption_units",[
("m³"),
("kWh")
])
async def test_when_gas_consumption_starting_at_latest_date_then_calculation_returned(consumption_units):
# Arrange
period_from = datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z")
period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z")
Expand All @@ -115,15 +135,20 @@ async def test_when_gas_consumption_starting_at_latest_date_then_calculation_ret
# Act
consumption = calculate_gas_consumption(
consumption_data,
latest_date
latest_date,
consumption_units
)

# Assert
assert consumption != None
assert consumption["last_calculated_timestamp"] == consumption_data[0]["interval_end"]

assert consumption["total_kwh"] == round(48 * 11.363, 3)
assert consumption["total_m3"] == 48 * 1
if consumption_units == "m³":
assert consumption["total_kwh"] == round(48 * 11.363, 3)
assert consumption["total_m3"] == 48 * 1
else:
assert consumption["total_kwh"] == 48 * 1
assert consumption["total_m3"] == round(48 * 0.088, 3)

assert len(consumption["consumptions"]) == len(consumption_data)

Expand All @@ -138,9 +163,15 @@ async def test_when_gas_consumption_starting_at_latest_date_then_calculation_ret
assert item["to"] == expected_valid_to

assert "consumption_m3" in item
assert item["consumption_m3"] == 1
if consumption_units == "m³":
assert item["consumption_m3"] == 1
else:
assert item["consumption_m3"] == 0.088

assert "consumption_kwh" in item
assert item["consumption_kwh"] == 11.363
if consumption_units == "m³":
assert item["consumption_kwh"] == 11.363
else:
assert item["consumption_kwh"] == 1

expected_valid_from = expected_valid_to

0 comments on commit 820abde

Please sign in to comment.