From 3ecf0839add5540dd9c6e8d9ba470b996e365931 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 22 Feb 2023 20:00:04 +0100 Subject: [PATCH 1/7] Add typing hints --- custom_components/prometheus_sensor/sensor.py | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/custom_components/prometheus_sensor/sensor.py b/custom_components/prometheus_sensor/sensor.py index 752a33b..7fec2bb 100644 --- a/custom_components/prometheus_sensor/sensor.py +++ b/custom_components/prometheus_sensor/sensor.py @@ -1,41 +1,45 @@ """Prometheus Sensor component.""" from datetime import timedelta import logging -from typing import Union +from typing import Dict, Final from urllib.parse import urljoin +import aiohttp import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, - STATE_CLASSES_SCHEMA, SensorEntity, ) +from homeassistant.components.sensor.const import ( + DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_URL, - CONF_UNIQUE_ID, STATE_PROBLEM, STATE_UNKNOWN, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - -DEFAULT_URL = "http://localhost:9090" -CONF_QUERIES = "queries" -CONF_EXPR = "expr" -CONF_STATE_CLASS = "state_class" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +_LOGGER: Final = logging.getLogger(__name__) -_LOGGER = logging.getLogger(__name__) +DEFAULT_URL: Final = "http://localhost:9090" +CONF_QUERIES: Final = "queries" +CONF_EXPR: Final = "expr" +CONF_STATE_CLASS: Final = "state_class" +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=60) -_QUERY_SCHEMA = vol.Schema( +_QUERY_SCHEMA: Final = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, @@ -46,7 +50,7 @@ } ) -PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, vol.Required(CONF_QUERIES): [_QUERY_SCHEMA], @@ -54,30 +58,33 @@ ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None +): """Set up the sensor platform.""" session = async_get_clientsession(hass) + url = config[CONF_URL] + prometheus = Prometheus(url, session) - prometheus = Prometheus(config.get(CONF_URL), session) - - sensors = [] - for query in config.get("queries", {}): - sensors.append(PrometheusSensor(prometheus, query)) - - _LOGGER.debug("Setting up %d prometheus sensors", len(sensors)) - - async_add_entities(sensors, update_before_add=True) + queries = config[CONF_QUERIES] + async_add_entities( + [PrometheusSensor(prometheus, query) for query in queries], + update_before_add=True + ) class Prometheus: """Wrapper for Prometheus API Requests.""" - def __init__(self, url: str, session) -> None: + def __init__(self, url: str, session: aiohttp.ClientSession) -> None: """Initialize the Prometheus API wrapper.""" self._session = session self._url = urljoin(f"{url}/", "api/v1/query") - async def query(self, expr) -> Union[str, float]: + async def query(self, expr: str) -> float | StateType: """Query expression response.""" response = await self._session.get(self._url, params={"query": expr}) if response.status != 200: @@ -111,12 +118,12 @@ async def query(self, expr) -> Union[str, float]: class PrometheusSensor(SensorEntity): """Sensor entity representing the result of a PromQL expression.""" - def __init__(self, prometheus: Prometheus, query: dict) -> None: + def __init__(self, prometheus: Prometheus, query: Dict[str, str]) -> None: """Initialize the sensor.""" - self._expr = query.get(CONF_EXPR) + self._expr = query[CONF_EXPR] self._prometheus: Prometheus = prometheus - self._attr_name = query.get(CONF_NAME) + self._attr_name = query[CONF_NAME] self._attr_unique_id = query.get(CONF_UNIQUE_ID) self._attr_native_unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = query.get(CONF_STATE_CLASS) From 06c07ec159c4222f2967bba62ffc82680b3ed9fc Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 22 Feb 2023 20:11:57 +0100 Subject: [PATCH 2/7] Apply black formatting --- custom_components/prometheus_sensor/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/prometheus_sensor/sensor.py b/custom_components/prometheus_sensor/sensor.py index 7fec2bb..79db60e 100644 --- a/custom_components/prometheus_sensor/sensor.py +++ b/custom_components/prometheus_sensor/sensor.py @@ -62,7 +62,7 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None + discovery_info: DiscoveryInfoType | None = None, ): """Set up the sensor platform.""" session = async_get_clientsession(hass) @@ -72,7 +72,7 @@ async def async_setup_platform( queries = config[CONF_QUERIES] async_add_entities( [PrometheusSensor(prometheus, query) for query in queries], - update_before_add=True + update_before_add=True, ) From d8ba71d38b09547b6acc8f86ec766020d08a4268 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 22 Feb 2023 22:47:06 +0100 Subject: [PATCH 3/7] Add flake.nix for pre-commit hooks --- .envrc | 1 + .gitignore | 2 + flake.lock | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 64 +++++++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c4b17d7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72b6a8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.pre-commit-config.yaml +.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..218e7cf --- /dev/null +++ b/flake.lock @@ -0,0 +1,151 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1715870890, + "narHash": "sha256-nacSOeXtUEM77Gn0G4bTdEOeFIrkCBXiyyFZtdGwuH0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "fa606cccd7b0ccebe2880051208e4a0f61bfc8c1", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1710765496, + "narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1710695816, + "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1716200252, + "narHash": "sha256-/h22mCdX1Od0Ej2yKEznRrk9+c4RzWOG9kLO91oCrl8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d3472a8728c882f4804ec751bfa960991d39c23b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f029a00 --- /dev/null +++ b/flake.nix @@ -0,0 +1,64 @@ +{ + description = "ha-prometheus-sensor"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + git-hooks.url = "github:cachix/git-hooks.nix"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + git-hooks, + flake-utils, + }: + + flake-utils.lib.eachSystem + [ + "aarch64-linux" + "x86_64-linux" + ] + ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + checks = { + pre-commit = git-hooks.lib.${system}.run { + src = ./.; + hooks = { + isort.enable = true; + ruff.enable = true; + ruff-format = { + enable = true; + entry = "${pkgs.ruff}/bin/ruff format"; + pass_filenames = false; + }; + }; + }; + }; + devShells.default = + with pkgs; + mkShell { + inherit (self.checks.${system}.pre-commit) shellHook; + + buildInputs = + [ + isort + pyright + ruff + ] + ++ (with python312.pkgs; [ + aiohttp + (toPythonModule pkgs.home-assistant) + voluptuous + ]); + }; + + formatter = pkgs.nixfmt-rfc-style; + } + ); +} From e6435e1f4232d8f9b67e708b4a1c55a63e66df14 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 22 Feb 2023 23:01:19 +0100 Subject: [PATCH 4/7] Configure isort --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..069630e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.isort] +profile = "black" +force_sort_within_sections = true +combine_as_imports = true +known_first_party = [ + "homeassistant" +] + From 1089e7564e0e4f098750aa430b6a91f3ea71f8e3 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 22 Feb 2023 22:25:13 +0100 Subject: [PATCH 5/7] workflows: Run flake check --- .github/workflows/checks.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/checks.yml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..9b54f6a --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,16 @@ +name: "Checks" + +on: + push: + pull_request: + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + - uses: DeterminateSystems/flake-checker-action@main + - run: nix flake check -L + From 3cf4d9212b42f51d5593f94c9d08b77a8f312488 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 20 May 2024 14:23:45 +0200 Subject: [PATCH 6/7] Fix formatting and typing issues --- custom_components/prometheus_sensor/sensor.py | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/custom_components/prometheus_sensor/sensor.py b/custom_components/prometheus_sensor/sensor.py index 79db60e..73117a5 100644 --- a/custom_components/prometheus_sensor/sensor.py +++ b/custom_components/prometheus_sensor/sensor.py @@ -1,20 +1,15 @@ """Prometheus Sensor component.""" + from datetime import timedelta import logging -from typing import Dict, Final +from typing import Final from urllib.parse import urljoin import aiohttp import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, - SensorEntity, -) -from homeassistant.components.sensor.const import ( - DEVICE_CLASSES_SCHEMA, - STATE_CLASSES_SCHEMA, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, @@ -27,6 +22,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import Throttle @@ -45,14 +43,14 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Required(CONF_EXPR): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Coerce(SensorDeviceClass), + vol.Optional(CONF_STATE_CLASS): vol.Coerce(SensorStateClass), } ) PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, # type: ignore vol.Required(CONF_QUERIES): [_QUERY_SCHEMA], } ) @@ -69,9 +67,19 @@ async def async_setup_platform( url = config[CONF_URL] prometheus = Prometheus(url, session) - queries = config[CONF_QUERIES] async_add_entities( - [PrometheusSensor(prometheus, query) for query in queries], + new_entities=[ + PrometheusSensor( + prometheus, + query[CONF_EXPR], + query[CONF_UNIQUE_ID], + query[CONF_NAME], + query.get(CONF_DEVICE_CLASS), + query.get(CONF_STATE_CLASS), + query.get(CONF_UNIT_OF_MEASUREMENT), + ) + for query in config[CONF_QUERIES] + ], update_before_add=True, ) @@ -118,18 +126,27 @@ async def query(self, expr: str) -> float | StateType: class PrometheusSensor(SensorEntity): """Sensor entity representing the result of a PromQL expression.""" - def __init__(self, prometheus: Prometheus, query: Dict[str, str]) -> None: + def __init__( + self, + prometheus: Prometheus, + expression: str, + unique_id: str, + device_name: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit_of_measurement: str | None, + ): """Initialize the sensor.""" - self._expr = query[CONF_EXPR] self._prometheus: Prometheus = prometheus + self._expression = expression - self._attr_name = query[CONF_NAME] - self._attr_unique_id = query.get(CONF_UNIQUE_ID) - self._attr_native_unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) - self._attr_state_class = query.get(CONF_STATE_CLASS) - self._attr_device_class = query.get(CONF_DEVICE_CLASS) + self._attr_device_class = device_class + self._attr_name = device_name + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_state_class = state_class + self._attr_unique_id = unique_id @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update state by executing query.""" - self._attr_native_value = await self._prometheus.query(self._expr) + self._attr_native_value = await self._prometheus.query(self._expression) From de1da619e8aeb1c8221fc5c348db2ce14f3e7186 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 20 May 2024 14:39:00 +0200 Subject: [PATCH 7/7] Add flake update action --- .github/workflows/update.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/update.yml diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 0000000..440cb5f --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,16 @@ +name: update-flake-lock +on: + workflow_dispatch: # allows manual triggering + schedule: + - cron: '0 17 * * 5' # Fridays at 17:00 + +jobs: + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@main