From c3a2526979e0a109ff6e50e2533c393648890757 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:50:28 +0100 Subject: [PATCH 1/8] add missing requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9b8320f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +colorlog==6.8.2 +homeassistant==2024.6.4 +pip>=21.3.1 From 852c6bca237021c490e5d0c109f585a9b82a8ae2 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:50:45 +0100 Subject: [PATCH 2/8] upgrade devcontainer from integration_blueprint --- .devcontainer/devcontainer.json | 52 ++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 01793e1..9f445e5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,31 +1,35 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "image": "ghcr.io/ludeeus/devcontainer/integration:stable", + "image": "mcr.microsoft.com/devcontainers/python:3.12", "name": "Nordpool 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 - } - + "postCreateCommand": "scripts/setup", + "customizations": { + "vscode": { + "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 + } + } + }, + "remoteUser": "vscode", + "features": {} } \ No newline at end of file From 89f2ae765c1ccfab334a3cc9bfe9a85c5806ebdd Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:50:53 +0100 Subject: [PATCH 3/8] fix nordpool package not resolving correctly use mount instead of relying on PYTHONPATH to find integration --- .devcontainer/devcontainer.json | 7 +++++++ scripts/dev | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9f445e5..00a6505 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,6 +5,13 @@ "appPort": [ "9123:8123" ], + // Mount 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. + "mounts": [ + "source=${localWorkspaceFolder}/custom_components,target=${containerWorkspaceFolder}/config/custom_components,type=bind,consistency=cached" + ], "postCreateCommand": "scripts/setup", "customizations": { "vscode": { diff --git a/scripts/dev b/scripts/dev index 8174444..67f86c9 100644 --- a/scripts/dev +++ b/scripts/dev @@ -10,12 +10,6 @@ if [[ ! -d "${PWD}/config" ]]; then 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" - echo $PWD # Start Home Assistant From 377fcfd34629a0efa724e1f63232fdaeb774e024 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:50:59 +0100 Subject: [PATCH 4/8] make script executable --- scripts/dev | 0 scripts/setup | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/dev mode change 100644 => 100755 scripts/setup diff --git a/scripts/dev b/scripts/dev old mode 100644 new mode 100755 diff --git a/scripts/setup b/scripts/setup old mode 100644 new mode 100755 From dd195377214d0d383187bdcc9264893f65468d1c Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:51:02 +0100 Subject: [PATCH 5/8] move configuration to config dir as per integration_blueprint --- .gitignore | 4 ++++ {.devcontainer => config}/configuration.yaml | 0 2 files changed, 4 insertions(+) rename {.devcontainer => config}/configuration.yaml (100%) diff --git a/.gitignore b/.gitignore index af6d502..32c93c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Home Assistant configuration +config/* +!config/configuration.yaml + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.devcontainer/configuration.yaml b/config/configuration.yaml similarity index 100% rename from .devcontainer/configuration.yaml rename to config/configuration.yaml From 1a1a7ab74b55b6758c37760680dd12c0f5f64fe0 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:51:05 +0100 Subject: [PATCH 6/8] remove deprecated tasks --- .vscode/tasks.json | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ab4ba8..e8062f2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,26 +4,8 @@ { "label": "Run Home Assistant on port 9123", "type": "shell", - "command": "container start", + "command": "scripts/dev", "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 specific version of Home Assistant", - "type": "shell", - "command": "container set-version", - "problemMatcher": [] - } ] } \ No newline at end of file From 4df5055bb2136d6fef655f935c2aaeef89483334 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Tue, 29 Oct 2024 21:51:09 +0100 Subject: [PATCH 7/8] add numpy dependency, seems to be required for onboarding --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9b8320f..75e6d3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ colorlog==6.8.2 homeassistant==2024.6.4 pip>=21.3.1 +numpy From ec3b21aa3c4a27b503c65dcd5b78e26997e894d7 Mon Sep 17 00:00:00 2001 From: bj00rn Date: Wed, 30 Oct 2024 21:37:16 +0100 Subject: [PATCH 8/8] add reconfiguration flow --- custom_components/nordpool/config_flow.py | 99 ++++++++++++++----- custom_components/nordpool/strings.json | 39 ++++++++ .../nordpool/translations/en.json | 16 ++- .../nordpool/translations/sv.json | 21 +++- 4 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 custom_components/nordpool/strings.json diff --git a/custom_components/nordpool/config_flow.py b/custom_components/nordpool/config_flow.py index 707c6b3..73b1142 100644 --- a/custom_components/nordpool/config_flow.py +++ b/custom_components/nordpool/config_flow.py @@ -1,11 +1,15 @@ """Adds config flow for nordpool.""" import logging import re +from typing import TYPE_CHECKING import voluptuous as vol from homeassistant import config_entries from homeassistant.helpers.template import Template +if TYPE_CHECKING: + from typing import Any, Mapping + from . import DOMAIN from .sensor import _PRICE_IN, _REGIONS, DEFAULT_TEMPLATE @@ -24,6 +28,7 @@ class NordpoolFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize.""" self._errors = {} + _config_entry = None async def async_step_user( self, user_input=None @@ -33,14 +38,38 @@ async def async_step_user( if user_input is not None: template_ok = False - if user_input["additional_costs"] in (None, ""): - user_input["additional_costs"] = DEFAULT_TEMPLATE + self._patch_template(user_input["additional_costs"]) + + template_ok = await self._valid_template(user_input["additional_costs"]) + if template_ok: + return self.async_create_entry(title="Nordpool", data=user_input) else: - # Lets try to remove the most common mistakes, this will still fail if the template - # was writte in notepad or something like that.. - user_input["additional_costs"] = re.sub( - r"\s{2,}", "", user_input["additional_costs"] - ) + self._errors["base"] = "invalid_template" + + return self.async_show_form( + step_id="user", + **self._get_form_data(user_input), + errors=self._errors, + ) + + async def async_step_reconfigure( + self, entry_data: "Mapping[str, Any]" + ) -> config_entries.ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._config_entry = config_entry + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: "dict[str, Any] | None" = None + ) -> config_entries.ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + + if user_input is not None: + template_ok = False + self._patch_template(user_input["additional_costs"]) template_ok = await self._valid_template(user_input["additional_costs"]) if template_ok: @@ -48,32 +77,50 @@ async def async_step_user( else: self._errors["base"] = "invalid_template" - data_schema = { - vol.Required("region", default=None): vol.In(regions), - vol.Optional("currency", default=""): vol.In(currencys), - vol.Optional("VAT", default=True): bool, - vol.Optional("precision", default=3): vol.Coerce(int), - vol.Optional("low_price_cutoff", default=1.0): vol.Coerce(float), - vol.Optional("price_in_cents", default=False): bool, - vol.Optional("price_type", default="kWh"): vol.In(price_types), - vol.Optional("additional_costs", default=""): str, - } + return self.async_update_reload_and_abort( + self._config_entry, + data=user_input, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure_confirm", **self._get_form_data(self._config_entry.data), + errors=self._errors + ) - placeholders = { - "region": regions, - "currency": currencys, - "price_type": price_types, + def _get_form_data(self, user_input: "Mapping[str, Any] | None"): + """Populate form data from user input and default values""" + if not user_input: + user_input = dict() + + data_schema = vol.Schema({ + vol.Required("region", default=user_input.get("region", None)): vol.In(regions), + vol.Required("currency", default=user_input.get("currency", None)): vol.In(currencys), + vol.Optional("VAT", default=user_input.get("VAT", True)): bool, + vol.Required("precision", default=user_input.get("precision", 3)): vol.Coerce(int), + vol.Required("low_price_cutoff", default=user_input.get("low_price_cutoff", 1.0)): vol.Coerce(float), + vol.Optional("price_in_cents", default=user_input.get("price_in_cents", False)): bool, + vol.Required("price_type", default=user_input.get("price_type", "kWh")): vol.In(price_types), + vol.Optional("additional_costs", default=user_input.get("additional_costs", "") or DEFAULT_TEMPLATE): str, + }) + + description_placeholders = { + "price_type": price_types[0], "additional_costs": "{{0.0|float}}", } - return self.async_show_form( - step_id="user", - data_schema=vol.Schema(data_schema), - description_placeholders=placeholders, - errors=self._errors, + return dict(description_placeholders=description_placeholders, data_schema=data_schema) + + def _patch_template(self, user_template: str): + """Fix common mistakes in template""" + # Lets try to remove the most common mistakes, this will still fail if the template + # was writte in notepad or something like that.. + user_template = re.sub( + r"\s{2,}", "", user_template ) async def _valid_template(self, user_template): + """Validate template""" try: # ut = Template(user_template, self.hass).async_render( diff --git a/custom_components/nordpool/strings.json b/custom_components/nordpool/strings.json new file mode 100644 index 0000000..7a39d7e --- /dev/null +++ b/custom_components/nordpool/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "title": "Nordpool", + "step": { + "user": { + "title": "Nordpool Sensor", + "description": "Setup a Nordpool sensor", + "data": { + "region": "Region", + "currency": "Currency", + "VAT": "Include VAT", + "precision": "Decimal rounding precision", + "low_price_cutoff": "Low price percentage", + "price_in_cents": "Price in cents", + "price_type": "Energy scale", + "additional_costs": "Template for additional costs" + }, + "reconfigure_confirm": { + "title": "Nordpool Sensor", + "description": "Reconfigure Nordpool sensor", + "data": { + "region": "Region", + "currency": "Currency", + "VAT": "Include VAT", + "precision": "Decimal rounding precision", + "low_price_cutoff": "Low price percentage", + "price_in_cents": "Price in cents", + "price_type": "Energy scale", + "additional_costs": "Template for additional costs" + } + } + } + }, + "error": { + "name_exists": "Name already exists", + "invalid_template": "The template is invalid, check https://github.com/custom-components/nordpool" + } + } +} \ No newline at end of file diff --git a/custom_components/nordpool/translations/en.json b/custom_components/nordpool/translations/en.json index fbb97e9..cc7fabf 100644 --- a/custom_components/nordpool/translations/en.json +++ b/custom_components/nordpool/translations/en.json @@ -15,6 +15,20 @@ "price_type": "Energy scale", "additional_costs": "Template for additional costs" } + }, + "reconfigure_confirm": { + "title": "Nordpool Sensor", + "description": "Reconfigure a Nordpool sensor", + "data": { + "region": "Region", + "currency": "Currency", + "VAT": "Include VAT", + "precision": "Decimal rounding precision", + "low_price_cutoff": "Low price percentage", + "price_in_cents": "Price in cents", + "price_type": "Energy scale", + "additional_costs": "Template for additional costs" + } } }, "error": { @@ -22,4 +36,4 @@ "invalid_template": "The template is invalid, check https://github.com/custom-components/nordpool" } } -} +} \ No newline at end of file diff --git a/custom_components/nordpool/translations/sv.json b/custom_components/nordpool/translations/sv.json index 0387354..ebd6051 100644 --- a/custom_components/nordpool/translations/sv.json +++ b/custom_components/nordpool/translations/sv.json @@ -3,12 +3,27 @@ "title": "Nord Pool", "step": { "user": { - "title": "Sensor", + "title": "Nordpool Sensor", "description": "Konfigurera en Nord Pool-sensor", "data": { "region": "Region", "friendly_name": "Visningsnamn", - "currency": "SEK", + "currency": "Valuta", + "VAT": "Inkludera moms", + "precision": "Hur många decimaler ska visas", + "low_price_cutoff": "Lägsta prisnivå", + "price_in_cents": "Pris i ören", + "price_type": "Prisformat", + "additional_costs": "Mall för ytterligare kostnader" + } + }, + "reconfigure_confirm": { + "title": "Nordpool Sensor", + "description": "Konfigurera en Nord Pool-sensor", + "data": { + "region": "Region", + "friendly_name": "Visningsnamn", + "currency": "Valuta", "VAT": "Inkludera moms", "precision": "Hur många decimaler ska visas", "low_price_cutoff": "Lägsta prisnivå", @@ -23,4 +38,4 @@ "invalid_template": "Mallen är ogiltig, se https://github.com/custom-components/nordpool" } } -} +} \ No newline at end of file