From 7b7c73edbbfca0a9bb6108f8c52375dc7b242cd9 Mon Sep 17 00:00:00 2001 From: Jean Swart Date: Sat, 16 Nov 2024 15:47:21 +0000 Subject: [PATCH 1/4] Update devcontainer configuration --- .devcontainer.json | 41 ++++++++++++++++++++++++++++++++ .devcontainer/configuration.yaml | 7 ------ .devcontainer/devcontainer.json | 30 ----------------------- .gitignore | 19 ++++++++++++++- .ruff.toml | 26 ++++++++++++++++++++ .vscode/tasks.json | 22 ++--------------- config/configuration.yaml | 12 ++++++++++ requirements.txt | 4 ++++ scripts/develop | 20 ++++++++++++++++ scripts/lint | 8 +++++++ scripts/setup | 7 ++++++ 11 files changed, 138 insertions(+), 58 deletions(-) create mode 100644 .devcontainer.json delete mode 100644 .devcontainer/configuration.yaml delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .ruff.toml create mode 100644 config/configuration.yaml create mode 100644 requirements.txt create mode 100644 scripts/develop create mode 100644 scripts/lint create mode 100644 scripts/setup diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..9a91ab6 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "ludeeus/integration_blueprint", + "image": "mcr.microsoft.com/devcontainers/python:3.12", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "github.vscode-pull-request-github", + "ms-python.python", + "ms-python.vscode-pylance", + "ryanluker.vscode-coverage-gutters" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnType": false, + "files.trimTrailingWhitespace": true, + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true, + "python.defaultInterpreterPath": "/usr/local/bin/python", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + } + } + } + }, + "remoteUser": "vscode", + "features": {} +} \ No newline at end of file diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml deleted file mode 100644 index 598b49b..0000000 --- a/.devcontainer/configuration.yaml +++ /dev/null @@ -1,7 +0,0 @@ -default_config: - -logger: - default: error - logs: - custom_components.eskom_loadshedding_interface: debug - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 7d6ca3a..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,30 +0,0 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. -{ - "image": "ghcr.io/ludeeus/devcontainer/integration:stable", - "name": "Blueprint integration development", - "context": "..", - "appPort": [ - "9123:8123" - ], - "postCreateCommand": "container install", - "extensions": [ - "ms-python.python", - "github.vscode-pull-request-github", - "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance" - ], - "settings": { - "files.eol": "\n", - "editor.tabSize": 4, - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/bin/python3", - "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ed8ebf5..0a8519a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,18 @@ -__pycache__ \ No newline at end of file +# artifacts +__pycache__ +.pytest* +*.egg-info +*/build/* +*/dist/* + + +# misc +.coverage +.vscode +coverage.xml +.ruff_cache + + +# Home Assistant configuration +config/* +!config/configuration.yaml \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..8ea6a71 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,26 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py312" + +[lint] +select = [ + "ALL", +] + +ignore = [ + "ANN101", # Missing type annotation for `self` in method + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "D203", # no-blank-line-before-class (incompatible with formatter) + "D212", # multi-line-summary-first-line (incompatible with formatter) + "COM812", # incompatible with formatter + "ISC001", # incompatible with formatter +] + +[lint.flake8-pytest-style] +fixture-parentheses = false + +[lint.pyupgrade] +keep-runtime-typing = true + +[lint.mccabe] +max-complexity = 25 \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 86c9651..3aa1c50 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,27 +2,9 @@ "version": "2.0.0", "tasks": [ { - "label": "Run Home Assistant on port 9123", + "label": "Run Home Assistant on port 8123", "type": "shell", - "command": "container start", - "problemMatcher": [] - }, - { - "label": "Run Home Assistant configuration against /config", - "type": "shell", - "command": "container check", - "problemMatcher": [] - }, - { - "label": "Upgrade Home Assistant to latest dev", - "type": "shell", - "command": "container install", - "problemMatcher": [] - }, - { - "label": "Install a spesific version of Home Assistant", - "type": "shell", - "command": "container set-version", + "command": "scripts/develop", "problemMatcher": [] } ] diff --git a/config/configuration.yaml b/config/configuration.yaml new file mode 100644 index 0000000..ccc8410 --- /dev/null +++ b/config/configuration.yaml @@ -0,0 +1,12 @@ +# https://www.home-assistant.io/integrations/default_config/ +default_config: + +# https://www.home-assistant.io/integrations/homeassistant/ +homeassistant: + debug: true + +# https://www.home-assistant.io/integrations/logger/ +logger: + default: info + logs: + custom_components.integration_blueprint: debug diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4fa552a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorlog==6.9.0 +homeassistant==2024.11.0 +pip>=21.3.1 +ruff==0.7.2 diff --git a/scripts/develop b/scripts/develop new file mode 100644 index 0000000..89eda50 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/integration_blueprint +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint new file mode 100644 index 0000000..5d68d15 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +ruff format . +ruff check . --fix diff --git a/scripts/setup b/scripts/setup new file mode 100644 index 0000000..141d19f --- /dev/null +++ b/scripts/setup @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --requirement requirements.txt From 7c741284838fb4be954821f1a1408b283d6c48cf Mon Sep 17 00:00:00 2001 From: Jean Swart Date: Sat, 16 Nov 2024 15:47:35 +0000 Subject: [PATCH 2/4] Update manifest --- custom_components/eskom_loadshedding/manifest.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/eskom_loadshedding/manifest.json b/custom_components/eskom_loadshedding/manifest.json index 53507a2..2ab224a 100644 --- a/custom_components/eskom_loadshedding/manifest.json +++ b/custom_components/eskom_loadshedding/manifest.json @@ -1,14 +1,14 @@ { "domain": "eskom_loadshedding", "name": "Eskom Loadshedding Interface", - "documentation": "https://github.com/swartjean/ha-eskom-loadshedding", - "issue_tracker": "https://github.com/swartjean/ha-eskom-loadshedding/issues", - "dependencies": [], - "config_flow": true, - "version": "1.1.2", "codeowners": [ "@swartjean" ], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/swartjean/ha-eskom-loadshedding", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/swartjean/ha-eskom-loadshedding/issues", "requirements": [], - "iot_class": "cloud_polling" + "version": "1.1.2" } \ No newline at end of file From 6e49a20daa81510cbc67c880681b5336b1ab520a Mon Sep 17 00:00:00 2001 From: Jean Swart Date: Sat, 16 Nov 2024 15:55:55 +0000 Subject: [PATCH 3/4] Code formatting --- .../eskom_loadshedding/__init__.py | 7 +++--- .../eskom_loadshedding/calendar.py | 23 +++++++++++-------- .../eskom_loadshedding/config_flow.py | 20 +++++++--------- custom_components/eskom_loadshedding/const.py | 1 + .../eskom_loadshedding/entity.py | 1 + .../eskom_loadshedding/eskom_interface.py | 7 +++--- .../eskom_loadshedding/sensor.py | 12 ++++++---- 7 files changed, 38 insertions(+), 33 deletions(-) diff --git a/custom_components/eskom_loadshedding/__init__.py b/custom_components/eskom_loadshedding/__init__.py index dc20846..fc1bdd5 100644 --- a/custom_components/eskom_loadshedding/__init__.py +++ b/custom_components/eskom_loadshedding/__init__.py @@ -4,9 +4,10 @@ For more details about this integration, please refer to https://github.com/swartjean/ha-eskom-loadshedding """ + import asyncio -from datetime import timedelta import logging +from datetime import timedelta from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -16,8 +17,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_SCAN_PERIOD, CONF_API_KEY, + CONF_SCAN_PERIOD, DEFAULT_SCAN_PERIOD, DOMAIN, PLATFORMS, @@ -57,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = coordinator - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if not entry.update_listeners: entry.add_update_listener(async_reload_entry) diff --git a/custom_components/eskom_loadshedding/calendar.py b/custom_components/eskom_loadshedding/calendar.py index d6d4406..233a92c 100644 --- a/custom_components/eskom_loadshedding/calendar.py +++ b/custom_components/eskom_loadshedding/calendar.py @@ -1,17 +1,18 @@ """Sensor platform for Eskom Loadshedding Interface.""" -from datetime import datetime, timedelta + import re +from datetime import datetime, timedelta from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import callback from .const import ( + DEFAULT_CALENDAR_SCAN_PERIOD, DOMAIN, LOCAL_EVENTS_ID, LOCAL_EVENTS_NAME, LOCAL_SCHEDULE_ID, LOCAL_SCHEDULE_NAME, - DEFAULT_CALENDAR_SCAN_PERIOD, ) from .entity import EskomEntity @@ -62,7 +63,8 @@ def name(self): @property def should_poll(self) -> bool: - """Enable polling for the entity. + """ + Enable polling for the entity. The coordinator is used to query the API, but polling is used to update the entity state more frequently. @@ -112,11 +114,11 @@ async def async_get_events( ) for event in events ] - else: - return [] + return [] async def async_update(self) -> None: - """Disable update behavior. + """ + Disable update behavior. Event updates are performed through the coordinator callback. This is simply used to evaluate the entity state """ @@ -145,7 +147,8 @@ def name(self): @property def should_poll(self) -> bool: - """Enable polling for the entity. + """ + Enable polling for the entity. The coordinator is used to query the API, but polling is used to update the entity state more frequently. @@ -215,11 +218,11 @@ async def async_get_events( ) return calendar_events - else: - return [] + return [] async def async_update(self) -> None: - """Disable update behavior. + """ + Disable update behavior. Event updates are performed through the coordinator callback. This is simply used to evaluate the entity state """ diff --git a/custom_components/eskom_loadshedding/config_flow.py b/custom_components/eskom_loadshedding/config_flow.py index 00b712e..bb7d3fb 100644 --- a/custom_components/eskom_loadshedding/config_flow.py +++ b/custom_components/eskom_loadshedding/config_flow.py @@ -1,15 +1,16 @@ """Adds config flow for the Eskom Loadshedding Interface.""" + from collections import OrderedDict +import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.selector import selector -import voluptuous as vol from .const import ( # pylint: disable=unused-import - CONF_SCAN_PERIOD, CONF_API_KEY, + CONF_SCAN_PERIOD, DEFAULT_SCAN_PERIOD, DOMAIN, MIN_SCAN_PERIOD, @@ -41,8 +42,7 @@ async def async_step_user(self, user_input=None): # Proceed to the next configuration step return await self.async_step_area_search() - else: - self._errors["base"] = "auth" + self._errors["base"] = "auth" return await self._show_user_config_form(user_input) @@ -91,8 +91,7 @@ async def async_step_area_selection(self, user_input=None): CONF_API_KEY: self.api_key, }, ) - else: - self._errors["base"] = "no_area_selection" + self._errors["base"] = "no_area_selection" # Reformat the areas as label/value pairs for the selector area_options = [ @@ -146,8 +145,7 @@ async def validate_key(self, api_key: str) -> bool: data = await interface.async_query_api("/api_allowance") if "error" in data: return False - else: - return True + return True except Exception: # pylint: disable=broad-except pass return False @@ -187,8 +185,7 @@ async def async_step_user(self, user_input=None): # Update all options self.options.update(user_input) return await self._update_options() - else: - self._errors["base"] = "auth" + self._errors["base"] = "auth" data_schema = OrderedDict() data_schema[ @@ -226,8 +223,7 @@ async def validate_key(self, api_key: str) -> bool: if "error" in data: return False - else: - return True + return True except Exception: # pylint: disable=broad-except pass return False diff --git a/custom_components/eskom_loadshedding/const.py b/custom_components/eskom_loadshedding/const.py index 34a7986..5a63e37 100644 --- a/custom_components/eskom_loadshedding/const.py +++ b/custom_components/eskom_loadshedding/const.py @@ -1,4 +1,5 @@ """Constants for eskom loadshedding interface""" + # Base component constants NAME = "Eskom Loadshedding Interface" DEVICE_NAME = "Loadshedding" diff --git a/custom_components/eskom_loadshedding/entity.py b/custom_components/eskom_loadshedding/entity.py index 64dfad5..ca0dc05 100644 --- a/custom_components/eskom_loadshedding/entity.py +++ b/custom_components/eskom_loadshedding/entity.py @@ -1,4 +1,5 @@ """EskomEntity class""" + from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEVICE_NAME, DOMAIN, VERSION diff --git a/custom_components/eskom_loadshedding/eskom_interface.py b/custom_components/eskom_loadshedding/eskom_interface.py index a0c4101..5f88800 100644 --- a/custom_components/eskom_loadshedding/eskom_interface.py +++ b/custom_components/eskom_loadshedding/eskom_interface.py @@ -1,4 +1,3 @@ -import asyncio import logging import socket @@ -25,7 +24,8 @@ def __init__( } async def async_query_api(self, endpoint: str, payload: dict = None): - """Queries a given endpoint on the EskomSePush API with the specified payload + """ + Queries a given endpoint on the EskomSePush API with the specified payload Args: endpoint (string): The endpoint of the EskomSePush API @@ -33,6 +33,7 @@ async def async_query_api(self, endpoint: str, payload: dict = None): Returns: The response object from the request + """ query_url = self.base_url + endpoint try: @@ -52,7 +53,7 @@ async def async_query_api(self, endpoint: str, payload: dict = None): # Re-raise the ClientResponseError to allow checking for valid headers during config # These will be caught by the DataUpdateCoordinator raise - except asyncio.TimeoutError as exception: + except TimeoutError as exception: _LOGGER.error( "Timeout fetching information from %s: %s", query_url, diff --git a/custom_components/eskom_loadshedding/sensor.py b/custom_components/eskom_loadshedding/sensor.py index 0ad94be..8963ed9 100644 --- a/custom_components/eskom_loadshedding/sensor.py +++ b/custom_components/eskom_loadshedding/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for Eskom Loadshedding Interface.""" -from datetime import datetime + import re +from datetime import datetime from homeassistant.components.sensor import SensorEntity @@ -156,10 +157,8 @@ def native_value(self): matches = re.findall(r"\d+", events[0]["note"]) if matches: return int(matches[0]) - else: - return events[0]["note"] - else: - return 0 + return events[0]["note"] + return 0 @property def icon(self): @@ -219,6 +218,8 @@ def native_value(self): if allowance: return int(allowance["limit"]) - int(allowance["count"]) + return None + @property def icon(self): """Return the icon of the sensor.""" @@ -236,3 +237,4 @@ def extra_state_attributes(self): "Limit": int(allowance["limit"]), "Type": allowance["type"], } + return None From da78a9e87284566ffb79b4d20b24bba4240db260 Mon Sep 17 00:00:00 2001 From: Jean Swart Date: Sat, 16 Nov 2024 16:01:58 +0000 Subject: [PATCH 4/4] Bump to v1.1.3 --- custom_components/eskom_loadshedding/const.py | 2 +- custom_components/eskom_loadshedding/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/eskom_loadshedding/const.py b/custom_components/eskom_loadshedding/const.py index 5a63e37..8601960 100644 --- a/custom_components/eskom_loadshedding/const.py +++ b/custom_components/eskom_loadshedding/const.py @@ -5,7 +5,7 @@ DEVICE_NAME = "Loadshedding" DOMAIN = "eskom_loadshedding" DOMAIN_DATA = f"{DOMAIN}_data" -VERSION = "1.1.2" +VERSION = "1.1.3" ISSUE_URL = "https://github.com/swartjean/ha-eskom-loadshedding/issues" diff --git a/custom_components/eskom_loadshedding/manifest.json b/custom_components/eskom_loadshedding/manifest.json index 2ab224a..3da83a5 100644 --- a/custom_components/eskom_loadshedding/manifest.json +++ b/custom_components/eskom_loadshedding/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/swartjean/ha-eskom-loadshedding/issues", "requirements": [], - "version": "1.1.2" + "version": "1.1.3" } \ No newline at end of file