From 57b0ecf80cd8109844487105d39fed9e0d484902 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Mon, 6 Mar 2023 00:02:49 +0100 Subject: [PATCH] fix: use prometheus metrics in hass integration --- .vscode/launch.json | 8 +++ .../estimenergy/config_flow.py | 42 ------------ custom_components/estimenergy/__init__.py | 10 +-- custom_components/estimenergy/const.py | 1 - custom_components/estimenergy/coordinator.py | 65 +++++++++---------- custom_components/estimenergy/sensor.py | 48 ++++---------- estimenergy/client/client.py | 5 ++ estimenergy/const.py | 4 +- 8 files changed, 62 insertions(+), 121 deletions(-) delete mode 100644 custom_components/custom_components/estimenergy/config_flow.py diff --git a/.vscode/launch.json b/.vscode/launch.json index d8366b3..b7b24a4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,14 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true + }, { "name": "Python: Module", "type": "python", diff --git a/custom_components/custom_components/estimenergy/config_flow.py b/custom_components/custom_components/estimenergy/config_flow.py deleted file mode 100644 index e646509..0000000 --- a/custom_components/custom_components/estimenergy/config_flow.py +++ /dev/null @@ -1,42 +0,0 @@ -from homeassistant import config_entries -import voluptuous as vol - -from homeassistant.data_entry_flow import FlowResult - -from estimenergy.const import DEFAULT_HOST, DEFAULT_PORT - -from .const import ( - DOMAIN, - CONF_HOST, - CONF_PORT, -) - - -class EstimEnergyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """EstimEnergy config flow.""" - - VERSION = 1 - - async def async_step_user(self, user_input=None) -> FlowResult: - if user_input is not None: - unique_id = f"estimenergy_{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" - - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry(title=unique_id, data=user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_HOST, - default=DEFAULT_HOST, - ): str, - vol.Required( - CONF_PORT, - default=DEFAULT_PORT, - ): int, - }, - ) - - return self.async_show_form(step_id="user", data_schema=data_schema) diff --git a/custom_components/estimenergy/__init__.py b/custom_components/estimenergy/__init__.py index cbc2189..0b7e9be 100644 --- a/custom_components/estimenergy/__init__.py +++ b/custom_components/estimenergy/__init__.py @@ -2,21 +2,24 @@ import logging import asyncio -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigType from homeassistant.core import HomeAssistant -from estimenergy.const import SENSOR_TYPES from .const import PLATFORM, DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the EstimEnergy component.""" + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up EstimEnergy from a config entry.""" + hass.data.setdefault(DOMAIN, {}) hass_data = dict(entry.data) hass.data[DOMAIN][entry.entry_id] = hass_data @@ -41,7 +44,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - # Remove config entry from domain. if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/custom_components/estimenergy/const.py b/custom_components/estimenergy/const.py index 531fc36..70d5e52 100644 --- a/custom_components/estimenergy/const.py +++ b/custom_components/estimenergy/const.py @@ -4,6 +4,5 @@ PLATFORM = Platform.SENSOR DOMAIN = "estimenergy" -CONF_NAME = "collector_name" CONF_HOST = "host" CONF_PORT = "port" diff --git a/custom_components/estimenergy/coordinator.py b/custom_components/estimenergy/coordinator.py index 8fd87ff..7a3b83b 100644 --- a/custom_components/estimenergy/coordinator.py +++ b/custom_components/estimenergy/coordinator.py @@ -2,11 +2,14 @@ import logging from datetime import timedelta +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from prometheus_client.parser import text_string_to_metric_families -from estimenergy_client import EstimEnergyClient - +from estimenergy.client import EstimEnergyClient +from estimenergy.const import METRICS, Metric _LOGGER = logging.getLogger(__name__) @@ -14,7 +17,7 @@ class EstimEnergyCoordinator(DataUpdateCoordinator): """Data coordinator for EstimEnergy.""" - def __init__(self, hass: HomeAssistant, host: str, port: int) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: super().__init__( hass, _LOGGER, @@ -22,41 +25,33 @@ def __init__(self, hass: HomeAssistant, host: str, port: int) -> None: ) self.hass = hass - self.host = host - self.port = port - self.client = EstimEnergyClient(host, port) - self.collector_id = None - self._data = None - self.collector = None - - async def initialize(self) -> None: - """Initialize the EstimEnergy API connection.""" - - collectors = await self.hass.async_add_executor_job(self.client.get_collectors) + self.host = entry.data[CONF_HOST] + self.port = entry.data[CONF_PORT] + self.client = EstimEnergyClient(self.host, self.port) - for collector in collectors: - if collector["name"] == self.name: - self.collector_id = collector["id"] - break + def get_value_for_metric(self, metric: Metric, samples: list): + """Get the value for a metric.""" - if not self.collector_id: - raise CollectorNotFoundError(self.name) + for sample in samples: + if sample.name == metric.json_key: + return sample.value + return None async def _async_update_data(self): """Refresh data from API.""" - if self.collector_id is None: - return None - - self.collector = await self.hass.async_add_executor_job( - self.client.get_collector, self.collector_id - ) - - return self.collector - - -class CollectorNotFoundError(Exception): - """Custom Exception for a collector not being found.""" - - def __init__(self, collector_name: str) -> None: - super().__init__(f"Collector with name {collector_name} not found!") + metrics_text = await self.hass.async_add_executor_job(self.client.get_metrics) + metrics = text_string_to_metric_families(metrics_text) + samples = [ + sample + for family in text_string_to_metric_families(metrics) + for sample in family.samples + if family.name in [metric.json_key for metric in METRICS] + ] + + return { + name: { + metric: self.get_value_for_metric(metric, samples) for metric in METRICS + } + for name in set([sample.labels["name"] for sample in samples]) + } diff --git a/custom_components/estimenergy/sensor.py b/custom_components/estimenergy/sensor.py index 2581da9..60af4a9 100644 --- a/custom_components/estimenergy/sensor.py +++ b/custom_components/estimenergy/sensor.py @@ -11,25 +11,11 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import CURRENCY_EURO, PERCENTAGE from homeassistant.const import UnitOfEnergy -from estimenergy.const import ( - JSON_BILLING_MONTH, - JSON_DATA, - JSON_DAY_KWH, - JSON_DAY_COST, - JSON_DAY_COST_DIFFERENCE, - JSON_MONTH_KWH_RAW, - JSON_YEAR_KWH_RAW, - JSON_MONTH_KWH, - JSON_MONTH_COST, - JSON_MONTH_COST_DIFFERENCE, - JSON_YEAR_KWH, - SENSOR_TYPE_FRIENDLY_NAME, - SENSOR_TYPE_JSON, - SENSOR_TYPE_UNIQUE_ID, - CONF_NAME, - CONF_HOST, - CONF_PORT, -) +from estimenergy.client import EstimEnergyClient +from estimenergy.const import METRICS, Metric, MetricType, MetricPeriod + +from .const import CONF_HOST, CONF_PORT +from .coordinator import EstimEnergyCoordinator _LOGGER = logging.getLogger(__name__) @@ -78,13 +64,7 @@ def __init__( @property def device_class(self) -> SensorDeviceClass | None: """Return the class of this entity.""" - if self.json_key in [ - JSON_DAY_KWH, - JSON_MONTH_KWH_RAW, - JSON_YEAR_KWH_RAW, - JSON_MONTH_KWH, - JSON_YEAR_KWH, - ]: + if self.metric.metric_type == MetricType.ENERGY: return SensorDeviceClass.ENERGY if self.metric.metric_type in [MetricType.COST, MetricType.COST_DIFFERENCE]: @@ -116,7 +96,7 @@ def last_reset(self) -> datetime | None: if self.metric.metric_period == MetricPeriod.TOTAL: return None - billing_month = self.coordinator.data[JSON_BILLING_MONTH] + billing_month = self.collector["billing_month"] now = datetime.now() return now.replace( @@ -134,12 +114,12 @@ def native_value(self) -> int | None: """Return the state of the sensor.""" if ( self.coordinator.data is None - or JSON_METRICS not in self.coordinator.data - or self.metric.json_key not in self.coordinator.data[JSON_METRICS] + or "metrics" not in self.coordinator.data + or self.metric.json_key not in self.coordinator.data["metrics"] ): return None - return self.coordinator.data[JSON_METRICS][self.metric.json_key] + return self.coordinator.data[self.collector["name"]][self.metric.json_key] @property def suggested_display_precision(self) -> int | None: @@ -149,13 +129,7 @@ def suggested_display_precision(self) -> int | None: @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" - if self.json_key in [ - JSON_DAY_KWH, - JSON_MONTH_KWH_RAW, - JSON_YEAR_KWH_RAW, - JSON_MONTH_KWH, - JSON_YEAR_KWH, - ]: + if self.device_class == SensorDeviceClass.ENERGY: return UnitOfEnergy.KILO_WATT_HOUR if self.device_class == SensorDeviceClass.MONETARY: diff --git a/estimenergy/client/client.py b/estimenergy/client/client.py index f4c3eeb..c70f201 100644 --- a/estimenergy/client/client.py +++ b/estimenergy/client/client.py @@ -26,3 +26,8 @@ async def async_get_collectors(self): url = f"http://{self.host}:{self.port}/collector" response = await requests.get(url) return response.json() + + def get_metrics(self): + url = f"http://{self.host}:{self.port}/metrics" + response = requests.get(url) + return response.text diff --git a/estimenergy/const.py b/estimenergy/const.py index 3c609cb..fa3fc94 100644 --- a/estimenergy/const.py +++ b/estimenergy/const.py @@ -30,7 +30,7 @@ def __init__(self, metric_type: MetricType, metric_period: MetricPeriod, is_pred @property def json_key(self) -> str: - return f"{self.metric_period.value[0]}_{self.metric_type.value[0]}{'_predicted' if self.is_predicted else ''}{'_raw' if self.is_raw else ''}" + return f"estimenergy_{self.metric_period.value[0]}_{self.metric_type.value[0]}{'_predicted' if self.is_predicted else ''}{'_raw' if self.is_raw else ''}" @property def friendly_name(self) -> str: @@ -38,7 +38,7 @@ def friendly_name(self) -> str: def create_gauge(self) -> Gauge: return Gauge( - f"estimenergy_{self.json_key}", + f"{self.json_key}", f"EstimEnergy {self.friendly_name}", ["name", "id"], )