Skip to content

Commit

Permalink
feat(sensor): Added sensors for accumulative cost of gas for the prev…
Browse files Browse the repository at this point in the history
…ious day
  • Loading branch information
BottlecapDave committed Dec 19, 2021
1 parent 6618246 commit 42a1408
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 42 deletions.
22 changes: 21 additions & 1 deletion custom_components/octopus_energy/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def async_get_rates(self, tariff_code, period_from, period_to):
return await self.async_get_day_night_rates(product_code, tariff_code, period_from, period_to)

async def async_electricity_consumption(self, mpan, serial_number, period_from, period_to):
"""Get the current electricity rates"""
"""Get the current electricity consumption"""
async with aiohttp.ClientSession() as client:
auth = aiohttp.BasicAuth(self._api_key, '')
url = f'{self._base_url}/v1/electricity-meter-points/{mpan}/meters/{serial_number}/consumption?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}'
Expand All @@ -131,6 +131,26 @@ async def async_electricity_consumption(self, mpan, serial_number, period_from,

return None

async def async_gas_rates(self, tariff_code, period_from, period_to):
"""Get the gas rates"""
tariff_parts = get_tariff_parts(tariff_code)
product_code = tariff_parts["product_code"]

results = []
async with aiohttp.ClientSession() as client:
auth = aiohttp.BasicAuth(self._api_key, '')
url = f'{self._base_url}/v1/products/{product_code}/gas-tariffs/{tariff_code}/standard-unit-rates?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}'
async with client.get(url, auth=auth) as response:
try:
# Disable content type check as sometimes it can report text/html
data = await response.json(content_type=None)
results = self.__process_rates(data, period_from, period_to, tariff_code)
except:
_LOGGER.error(f'Failed to extract standard gas rates: {url}')
raise

return results

async def async_gas_consumption(self, mprn, serial_number, period_from, period_to):
"""Get the current gas rates"""
async with aiohttp.ClientSession() as client:
Expand Down
191 changes: 150 additions & 41 deletions custom_components/octopus_energy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,17 @@ async def async_setup_default_sensors(hass, entry, async_add_entities):
if async_get_active_tariff_code(point["agreements"], client) != None:
for meter in point["meters"]:
coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"])
entities.append(OctopusEnergyPreviousAccumulativeElectricityReading(coordinator, client, point["mpan"], meter["serial_number"]))
entities.append(OctopusEnergyPreviousAccumulativeElectricityReading(coordinator, point["mpan"], meter["serial_number"]))
entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(coordinator, client, tariff_code, point["mpan"], meter["serial_number"]))

if len(account_info["gas_meter_points"]) > 0:
for point in account_info["gas_meter_points"]:
# We only care about points that have active agreements
if async_get_active_tariff_code(point["agreements"], client) != None:
for meter in point["meters"]:
entities.append(OctopusEnergyPreviousAccumulativeGasReading(client, point["mprn"], meter["serial_number"], is_smets1))
coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"])
entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"], is_smets1))
entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, tariff_code, point["mprn"], meter["serial_number"], is_smets1))

async_add_entities(entities, True)

Expand Down Expand Up @@ -263,20 +265,19 @@ def state(self):
class OctopusEnergyPreviousAccumulativeElectricityReading(CoordinatorEntity, SensorEntity):
"""Sensor for displaying the previous days accumulative electricity reading."""

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

self._mprn = mprn
self._mpan = mpan
self._serial_number = serial_number
self._client = client

self._attributes = {
"MPRN": mprn,
"MPAN": mpan,
"Serial Number": serial_number
}

self._state = None
self._state = 0
self._latest_date = None

@property
Expand Down Expand Up @@ -320,7 +321,7 @@ def state(self):
if (self.coordinator.data != None and len(self.coordinator.data) == 48):

if (self._latest_date != self.coordinator.data[-1]["interval_end"]):
_LOGGER.info(f"Calculating previous electricity consumption for '{self._mprn}/{self._serial_number}'...")
_LOGGER.info(f"Calculating previous electricity consumption for '{self._mpan}/{self._serial_number}'...")

total = 0
for consumption in self.coordinator.data:
Expand All @@ -334,17 +335,17 @@ def state(self):
class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, SensorEntity):
"""Sensor for displaying the previous days accumulative electricity cost."""

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

self._mprn = mprn
self._mpan = mpan
self._serial_number = serial_number
self._client = client
self._tariff_code = tariff_code

self._attributes = {
"MPRN": mprn,
"MPAN": mpan,
"Serial Number": serial_number
}

Expand Down Expand Up @@ -400,7 +401,7 @@ async def async_update(self):

# Only calculate our consumption if our data has changed
if (self._latest_date != self.coordinator.data[-1]["interval_end"]):
_LOGGER.debug(f"Calculating previous electricity cost for '{self._mprn}/{self._serial_number}'...")
_LOGGER.debug(f"Calculating previous electricity cost for '{self._mpan}/{self._serial_number}'...")

current_datetime = now()
period_from = as_utc((current_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0))
Expand Down Expand Up @@ -437,31 +438,32 @@ async def async_update(self):
self._latest_date = self.coordinator.data[-1]["interval_end"]

self._attributes = {
"MPRN": self._mprn,
"MPAN": self._mpan,
"Serial Number": self._serial_number,
"Standard Charge": f'{standard_charge}p',
"Total Cost Without Standard Charge": f'£{total_cost}',
"Charges": charges
}

class OctopusEnergyPreviousAccumulativeGasReading(SensorEntity):
class OctopusEnergyPreviousAccumulativeGasReading(CoordinatorEntity, SensorEntity):
"""Sensor for displaying the previous days accumulative gas reading."""

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

self._mprn = mprn
self._serial_number = serial_number
self._is_smets1_meter = is_smets1_meter
self._client = client

self._attributes = {
"MPRN": mprn,
"Serial Number": serial_number,
"Is SMETS1 Meter": is_smets1_meter
}

self._state = None
self._data = []
self._state = 0
self._latest_date = None

@property
def unique_id(self):
Expand Down Expand Up @@ -500,32 +502,139 @@ def extra_state_attributes(self):

@property
def state(self):
"""Native value of the sensor."""
return self._state

async def async_update(self):
"""Retrieve the previous days accumulative consumption"""

current_datetime = now()

# We only need to do this once a day, unless we don't have enough data for the day therefore we want to retrieve it
# every hour until we have enough data for the day
if (current_datetime.hour == 0 and current_datetime.minute == 0) or self._state == None or (current_datetime.minute % 60 == 0 and len(self._data) != 48):
_LOGGER.debug('Updating OctopusEnergyPreviousAccumulativeGasReading')
if (self.coordinator.data != None and len(self.coordinator.data) == 48):

if (self._latest_date != self.coordinator.data[-1]["interval_end"]):
_LOGGER.info(f"Calculating previous gas consumption for '{self._mprn}/{self._serial_number}'...")

period_from = as_utc((current_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0))
period_to = as_utc(current_datetime.replace(hour=0, minute=0, second=0, microsecond=0))
data = await self._client.async_gas_consumption(self._mprn, self._serial_number, period_from, period_to)
if data != None:
total = 0
for item in data:
total = total + item["consumption"]
for consumption in self.coordinator.data:
total = total + consumption["consumption"]

self._state = total
self._data = data
else:
self._state = 0
self._data = []
self._latest_date = self.coordinator.data[-1]["interval_end"]

if self._is_smets1_meter:
self._state = convert_kwh_to_m3(self._state)

return self._state

class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, SensorEntity):
"""Sensor for displaying the previous days accumulative gas cost."""

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

self._mprn = mprn
self._serial_number = serial_number
self._client = client
self._tariff_code = tariff_code
self._is_smets1_meter = is_smets1_meter

self._attributes = {
"MPRN": mprn,
"Serial Number": serial_number
}

self._state = 0
self._latest_date = None

@property
def unique_id(self):
"""The id of the sensor."""
return f"octopus_energy_gas_{self._serial_number}_previous_accumulative_cost"

@property
def name(self):
"""Name of the sensor."""
return f"Octopus Energy Gas {self._serial_number} Previous Accumulative Cost"

@property
def device_class(self):
"""The type of sensor"""
return DEVICE_CLASS_MONETARY

@property
def state_class(self):
"""The state class of sensor"""
return STATE_CLASS_TOTAL_INCREASING

@property
def unit_of_measurement(self):
"""The unit of measurement of sensor"""
return "GBP"

@property
def icon(self):
"""Icon of the sensor."""
return "mdi:currency-usd"

@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
return self._attributes

@property
def should_poll(self):
return True

@property
def state(self):
"""Retrieve the previously calculated state"""
return self._state

async def async_update(self):
if (self.coordinator.data != None and len(self.coordinator.data) == 48):

# Only calculate our consumption if our data has changed
if (self._latest_date != self.coordinator.data[-1]["interval_end"]):
_LOGGER.debug(f"Calculating previous gas cost for '{self._mprn}/{self._serial_number}'...")

current_datetime = now()
period_from = as_utc((current_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0))
period_to = as_utc(current_datetime.replace(hour=0, minute=0, second=0, microsecond=0))

rates = await self._client.async_gas_rates(self._tariff_code, period_from, period_to)
standard_charge_result = await self._client.async_get_gas_standing_charges(self._tariff_code, period_from, period_to)
standard_charge = standard_charge_result["value_inc_vat"]

if self._is_smets1_meter:
self._state = convert_kwh_to_m3(self._state)
charges = []
total_cost_in_pence = 0
for consumption in self.coordinator.data:
value = consumption["consumption"]

if self._is_smets1_meter:
value = convert_kwh_to_m3(value)

consumption_from = consumption["interval_start"]
consumption_to = consumption["interval_end"]

rate = next(rate for rate in rates if rate["valid_from"] == consumption_from and rate["valid_to"] == consumption_to)
if rate == None:
raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to}")

cost = (rate["value_inc_vat"] * value)
total_cost_in_pence = total_cost_in_pence + cost

charges.append({
"From": rate["valid_from"],
"To": rate["valid_to"],
"Rate": f'{rate["value_inc_vat"]}p',
"Consumption": f'{value} kWh',
"Cost": f'£{round(cost / 100, 2)}'
})

total_cost = round(total_cost_in_pence / 100, 2)
self._state = round((total_cost_in_pence + standard_charge) / 100, 2)
self._latest_date = self.coordinator.data[-1]["interval_end"]

self._attributes = {
"MPRN": self._mprn,
"Serial Number": self._serial_number,
"Standard Charge": f'{standard_charge}p',
"Total Cost Without Standard Charge": f'£{total_cost}',
"Charges": charges
}

0 comments on commit 42a1408

Please sign in to comment.