diff --git a/custom_components/meridian_energy/__init__.py b/custom_components/meridian_energy/__init__.py index d444954..0c5acea 100644 --- a/custom_components/meridian_energy/__init__.py +++ b/custom_components/meridian_energy/__init__.py @@ -1,11 +1,11 @@ -"""Support for Meridian Energy sensors""" +"""Support for Meridian Energy sensors.""" + import logging from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from .api import MeridianEnergyApi -from .const import DOMAIN, SENSOR_NAME, PLATFORMS +from .const import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -21,4 +21,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) \ No newline at end of file + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/meridian_energy/api.py b/custom_components/meridian_energy/api.py index f8a2cf5..301632a 100644 --- a/custom_components/meridian_energy/api.py +++ b/custom_components/meridian_energy/api.py @@ -1,7 +1,7 @@ -"""Meridian Energy API""" +"""Meridian Energy API.""" + import logging import requests -import json from datetime import datetime, timedelta from bs4 import BeautifulSoup @@ -11,10 +11,13 @@ class MeridianEnergyApi: + """Define the Meridian Energy API.""" + def __init__(self, email, password): + """Initialise the API.""" self._email = email self._password = password - self._url_base = 'https://secure.meridianenergy.co.nz/' + self._url_base = "https://secure.meridianenergy.co.nz/" self._token = None self._data = None self._session = requests.Session() @@ -24,43 +27,55 @@ def token(self): response = self._session.get(self._url_base) if response.status_code == 200: - soup = BeautifulSoup(response.text, 'html.parser') - self._token = soup.find('input', {'name': 'authenticity_token'})['value'] + soup = BeautifulSoup(response.text, "html.parser") + self._token = soup.find("input", {"name": "authenticity_token"})["value"] _LOGGER.debug(f"Authenticity Token: {self._token}") self.login() else: _LOGGER.error("Failed to retrieve the token page.") def login(self): - """Login to the Meridian Energy API.""" + """Login to the API.""" result = False form_data = { "authenticity_token": self._token, "email": self._email, "password": self._password, - "commit": "Login" + "commit": "Login", } - loginResult = self._session.post(self._url_base + "customer/login", data=form_data) + loginResult = self._session.post( + self._url_base + "customer/login", data=form_data + ) if loginResult.status_code == 200: - _LOGGER.debug('Logged in') + _LOGGER.debug("Logged in") self.get_data() result = True else: - _LOGGER.error('Could not login') + _LOGGER.error("Could not login") return result def get_data(self): + """Get data from the API.""" # Get todays date - today = datetime.now().strftime('%d/%m/%Y') - seven_days_ago = (datetime.now() - timedelta(days=365)).strftime('%d/%m/%Y') - - response = self._session.get(self._url_base + "reports/consumption_data/detailed_export?date_from=" + seven_days_ago + "&date_to=" + today + "&all_icps=&download=true") + today = datetime.now().strftime("%d/%m/%Y") + seven_days_ago = (datetime.now() - timedelta(days=365)).strftime("%d/%m/%Y") + + response = self._session.get( + self._url_base + + "reports/consumption_data/detailed_export?date_from=" + + seven_days_ago + + "&date_to=" + + today + + "&all_icps=&download=true" + ) if response.status_code == 200: data = response.text if not data: - _LOGGER.warning('Fetched consumption successfully but there was no data') + _LOGGER.warning( + "Fetched consumption successfully but there was no data" + ) return False return data else: - _LOGGER.error('Could not fetch consumption') - return data \ No newline at end of file + _LOGGER.error("Could not fetch consumption") + return data diff --git a/custom_components/meridian_energy/config_flow.py b/custom_components/meridian_energy/config_flow.py index 2c5b996..7a46b82 100644 --- a/custom_components/meridian_energy/config_flow.py +++ b/custom_components/meridian_energy/config_flow.py @@ -1,31 +1,36 @@ +"""Config flow for Meridian Energy integration.""" + from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD import homeassistant.helpers.config_validation as cv import voluptuous as vol -from .const import ( - DOMAIN, - SENSOR_NAME -) +from .const import DOMAIN, SENSOR_NAME + @config_entries.HANDLERS.register(DOMAIN) class MeridianConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Define the config flow.""" + VERSION = 1 async def async_step_user(self, user_input=None): + """Show user form.""" if user_input is not None: return self.async_create_entry( title=SENSOR_NAME, data={ CONF_EMAIL: user_input[CONF_EMAIL], - CONF_PASSWORD: user_input[CONF_PASSWORD] - } + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, ) return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string - }) - ) \ No newline at end of file + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ), + ) diff --git a/custom_components/meridian_energy/const.py b/custom_components/meridian_energy/const.py index 01868c3..c0816d0 100644 --- a/custom_components/meridian_energy/const.py +++ b/custom_components/meridian_energy/const.py @@ -1,9 +1,10 @@ -"""Constants for the Meridian Energy sensors""" +"""Constants for the Meridian Energy sensors.""" + from homeassistant.const import Platform -DOMAIN = 'meridian_energy' -SENSOR_NAME = 'Meridian Energy' +DOMAIN = "meridian_energy" +SENSOR_NAME = "Meridian Energy" PLATFORMS = [ Platform.SENSOR, -] \ No newline at end of file +] diff --git a/custom_components/meridian_energy/sensor.py b/custom_components/meridian_energy/sensor.py index 50bb9bb..a189125 100644 --- a/custom_components/meridian_energy/sensor.py +++ b/custom_components/meridian_energy/sensor.py @@ -1,4 +1,5 @@ -"""Meridian Energy sensors""" +"""Meridian Energy sensors.""" + from datetime import datetime, timedelta import csv @@ -12,31 +13,18 @@ from homeassistant.core import HomeAssistant from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.components.recorder.models import StatisticData, StatisticMetaData -from homeassistant.const import ENERGY_KILO_WATT_HOUR, CURRENCY_DOLLAR +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.components.recorder.statistics import ( async_add_external_statistics, - clear_statistics, - #day_start_end, - get_last_statistics, - list_statistic_ids, - #month_start_end, - statistics_during_period, ) -import homeassistant.util.dt as dt_util -import math from .api import MeridianEnergyApi -from .const import ( - DOMAIN, - SENSOR_NAME -) +from .const import DOMAIN, SENSOR_NAME NAME = DOMAIN ISSUEURL = "https://github.com/codyc1515/ha-meridian-energy/issues" @@ -52,39 +40,41 @@ _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) SCAN_INTERVAL = timedelta(hours=3) + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info=None + discovery_info=None, ): + """Asynchrounously set-up the entry.""" email = entry.data.get(CONF_EMAIL) password = entry.data.get(CONF_PASSWORD) - + api = MeridianEnergyApi(email, password) - _LOGGER.debug('Setting up sensor(s)...') + _LOGGER.debug("Setting up sensor(s)...") sensors = [] sensors.append(MeridianEnergyUsageSensor(SENSOR_NAME, api)) async_add_entities(sensors, True) + class MeridianEnergyUsageSensor(SensorEntity): + """Define Meridian Energy Usage sensor.""" + def __init__(self, name, api): + """Intialise Meridian Energy Usage sensor.""" self._name = name self._icon = "mdi:meter-electric" self._state = 0 - #self._unit_of_measurement = 'kWh' self._unique_id = DOMAIN - #self._device_class = "energy" - #self._state_class = "total" self._state_attributes = {} self._api = api @@ -108,28 +98,14 @@ def extra_state_attributes(self): """Return the state attributes of the sensor.""" return self._state_attributes - #@property - #def unit_of_measurement(self): - # """Return the unit of measurement.""" - # return self._unit_of_measurement - # - #@property - #def state_class(self): - # """Return the state class.""" - # return self._state_class - # - #@property - #def device_class(self): - # """Return the device class.""" - # return self._device_class - @property def unique_id(self): """Return the unique id.""" return self._unique_id def update(self): - _LOGGER.debug('Beginning usage update') + """Update the sensor data.""" + _LOGGER.debug("Beginning usage update") solarStatistics = [] solarRunningSum = 0 @@ -152,77 +128,77 @@ def update(self): for row in csv_file: # Accessing columns by index in each row if len(row) >= 2: # Checking if there are at least two columns - if row[0] == 'HDR': - _LOGGER.debug('HDR line arrived') + if row[0] == "HDR": + _LOGGER.debug("HDR line arrived") continue - elif row[0] == 'DET': - _LOGGER.debug('DET line arrived') - + elif row[0] == "DET": + _LOGGER.debug("DET line arrived") + # Row definitions from EIEP document 13A (https://www.ea.govt.nz/documents/182/EIEP_13A_Electricity_conveyed_information_for_consumers.pdf) - record_type = row[0] - consumer_authorisation_code = row[1] - icp_identifier = row[2] - response_code = row[3] - nzdt_adjustment = row[4] - metering_component_serial_number = row[5] energy_flow_direction = row[6] - register_content_code = row[7] - period_of_availability = row[8] # refer below for date logic of row[9] - read_period_end_date_time = row[10] read_status = row[11] unit_quantity_active_energy_volume = row[12] - unit_quantity_reactive_energy_volume = row[13] - + # Assuming row[9] contains the date in the format 'dd/mm/YYYY HH:MM:SS' read_period_start_date_time = row[9] # Assuming tz is your timezone (e.g., pytz.timezone('Your/Timezone')) - tz = timezone('Pacific/Auckland') + tz = timezone("Pacific/Auckland") # Parse the date string into a datetime object - start_date = datetime.strptime(read_period_start_date_time, '%d/%m/%Y %H:%M:%S') + start_date = datetime.strptime( + read_period_start_date_time, "%d/%m/%Y %H:%M:%S" + ) # Localize the datetime object start_date = tz.localize(start_date) - + # Exclude any readings that are at the 59th minute if start_date.minute == 59: continue # Round down to the nearest hour as HA can only handle hourly rounded_date = start_date.replace(minute=0, second=0, microsecond=0) - + # Skip any estimated reads if read_status != "RD": - _LOGGER.debug('HDR line skipped as its estimated') + _LOGGER.debug("HDR line skipped as its estimated") continue - + # Process solar export channels - if energy_flow_direction == 'I': - solarRunningSum = solarRunningSum + float(unit_quantity_active_energy_volume) - solarStatistics.append(StatisticData( - start=rounded_date, - sum=solarRunningSum - )) - + if energy_flow_direction == "I": + solarRunningSum = solarRunningSum + float( + unit_quantity_active_energy_volume + ) + solarStatistics.append( + StatisticData(start=rounded_date, sum=solarRunningSum) + ) + # Process regular channels else: # Night rate channel - if rounded_date.time() >= datetime.strptime("21:00", "%H:%M").time() or rounded_date.time() < datetime.strptime("07:00", "%H:%M").time(): - nightRunningSum = nightRunningSum + float(unit_quantity_active_energy_volume) - nightStatistics.append(StatisticData( - start=rounded_date, - sum=nightRunningSum - )) - + if ( + rounded_date.time() + >= datetime.strptime("21:00", "%H:%M").time() + or rounded_date.time() + < datetime.strptime("07:00", "%H:%M").time() + ): + nightRunningSum = nightRunningSum + float( + unit_quantity_active_energy_volume + ) + nightStatistics.append( + StatisticData(start=rounded_date, sum=nightRunningSum) + ) + # Day rate channel else: - dayRunningSum = dayRunningSum + float(unit_quantity_active_energy_volume) - dayStatistics.append(StatisticData( - start=rounded_date, - sum=dayRunningSum - )) + dayRunningSum = dayRunningSum + float( + unit_quantity_active_energy_volume + ) + dayStatistics.append( + StatisticData(start=rounded_date, sum=dayRunningSum) + ) else: _LOGGER.warning("Not enough columns in this row") @@ -232,7 +208,7 @@ def update(self): name=f"{SENSOR_NAME} (Solar Export)", source=DOMAIN, statistic_id=f"{DOMAIN}:return_to_grid", - unit_of_measurement=ENERGY_KILO_WATT_HOUR + unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) async_add_external_statistics(self.hass, solarMetadata, solarStatistics) @@ -242,7 +218,7 @@ def update(self): name=f"{SENSOR_NAME} (Day)", source=DOMAIN, statistic_id=f"{DOMAIN}:consumption_day", - unit_of_measurement=ENERGY_KILO_WATT_HOUR + unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) async_add_external_statistics(self.hass, dayMetadata, dayStatistics) @@ -252,6 +228,6 @@ def update(self): name=f"{SENSOR_NAME} (Night)", source=DOMAIN, statistic_id=f"{DOMAIN}:consumption_night", - unit_of_measurement=ENERGY_KILO_WATT_HOUR + unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) - async_add_external_statistics(self.hass, nightMetadata, nightStatistics) \ No newline at end of file + async_add_external_statistics(self.hass, nightMetadata, nightStatistics)