diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 01793e1..00a6505 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,31 +1,42 @@ // 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" + // 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" ], - "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 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/.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 diff --git a/.devcontainer/configuration.yaml b/config/configuration.yaml similarity index 100% rename from .devcontainer/configuration.yaml rename to config/configuration.yaml 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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..75e6d3f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorlog==6.8.2 +homeassistant==2024.6.4 +pip>=21.3.1 +numpy diff --git a/scripts/dev b/scripts/dev old mode 100644 new mode 100755 index 8174444..67f86c9 --- 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 diff --git a/scripts/setup b/scripts/setup old mode 100644 new mode 100755