diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 0000000..5f7a071 --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,14 @@ +name: Validate with hassfest + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v3" + - uses: "home-assistant/actions/hassfest@master" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..480766c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +--- +name: "Release" + +on: + release: + types: + - published + +jobs: + release: + name: "Release default_config_disabler" + runs-on: ubuntu-latest + steps: + - name: "⬇️ Checkout the repository" + uses: actions/checkout@v3 + + - name: "🔢 Adjust version number" + shell: bash + run: | + version="${{ github.event.release.tag_name }}" + version="${version,,}" + version="${version#v}" + yq e -P -o=json \ + -i ".version = \"${version}\"" \ + "${{ github.workspace }}/custom_components/default_config_disabler/manifest.json" + - name: "📦 Created zipped release package" + shell: bash + run: | + cd "${{ github.workspace }}/custom_components/default_config_disabler" + zip default_config_disabler.zip -r ./ + - name: "⬆️ Upload zip to release" + uses: softprops/action-gh-release@v0.1.15 + with: + files: ${{ github.workspace }}/custom_components/default_config_disabler/default_config_disabler.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..71734df --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) + +Home Assistant integration to disable selected components from default_config. +See popular feature request at https://community.home-assistant.io/t/why-the-heck-is-default-config-so-difficult-to-customize/220112 + +Note: After a Home Assistant update this integration will re-disable selected components and automatically restart Home Assistant. + +# Installation + +## HACS +1. [Add](http://homeassistant.local:8123/hacs/integrations) custom integrations repository: https://github.com/tronikos/default_config_disabler +2. Select "Default Config Disabler" in the Integration tab and click download +3. Restart Home Assistant +4. Enable the integration + +## Manual +1. Copy directory `custom_components/default_config_disabler` to your `/custom_components` directory +2. Restart Home-Assistant +3. Enable the integration + +## Enable the integration +1. Go to [Settings / Devices & Services / Integrations](http://homeassistant.local:8123/config/integrations). Click **+ ADD INTEGRATION** +2. Search for "Default Config Disabler" and click on it +3. Restart Home Assistant + +# Configuration +1. Go to [Settings / Devices & Services / Integrations](http://homeassistant.local:8123/config/integrations) +2. Select "Default Config Disabler" and click on "Configure" +3. Select the default_config components you want to disable diff --git a/custom_components/default_config_disabler/__init__.py b/custom_components/default_config_disabler/__init__.py new file mode 100644 index 0000000..df17d9a --- /dev/null +++ b/custom_components/default_config_disabler/__init__.py @@ -0,0 +1,63 @@ +"""The Default Config Disabler integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SERVICE_HOMEASSISTANT_RESTART +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant + +from .const import CONF_COMPONENTS_TO_DISABLE +from .helpers import ( + backup_original_default_config_manifest, + load_default_config_manifest, + load_original_default_config_manifest, + restore_original_default_config_manifest, + save_default_config_manifest, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Default Config Disabler from a config entry.""" + disabled_components = entry.options.get(CONF_COMPONENTS_TO_DISABLE, []) + + backup_original_default_config_manifest() + + new_manifest = load_original_default_config_manifest() + for disabled_component in disabled_components: + if disabled_component in new_manifest["dependencies"]: + new_manifest["dependencies"].remove(disabled_component) + + current_manifest = load_default_config_manifest() + + if new_manifest == current_manifest: + _LOGGER.info( + "Components: %s are already removed from default_config", + disabled_components, + ) + else: + save_default_config_manifest(new_manifest) + _LOGGER.warning( + "Components: %s were removed from default_config", disabled_components + ) + _LOGGER.warning("Restarting Home Assistant to use the updated default_config") + await hass.services.async_call(HA_DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if restore_original_default_config_manifest(): + _LOGGER.warning("Restarting Home Assistant to use the original default_config") + await hass.services.async_call(HA_DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/default_config_disabler/config_flow.py b/custom_components/default_config_disabler/config_flow.py new file mode 100644 index 0000000..b740a2f --- /dev/null +++ b/custom_components/default_config_disabler/config_flow.py @@ -0,0 +1,69 @@ +"""Config flow for Default Config Disabler integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import CONF_COMPONENTS_TO_DISABLE, DOMAIN, NAME +from .helpers import get_default_config_components + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Default Config Disabler.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title=NAME, data=user_input) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create the options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Default Config Disabler options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_COMPONENTS_TO_DISABLE, + default=self.config_entry.options.get( + CONF_COMPONENTS_TO_DISABLE, [] + ), + ): cv.multi_select(get_default_config_components()), + } + ), + ) diff --git a/custom_components/default_config_disabler/const.py b/custom_components/default_config_disabler/const.py new file mode 100644 index 0000000..2148c7e --- /dev/null +++ b/custom_components/default_config_disabler/const.py @@ -0,0 +1,7 @@ +"""Constants for the Default Config Disabler integration.""" +from typing import Final + +DOMAIN: Final = "default_config_disabler" +NAME: Final = "Default Config Disabler" + +CONF_COMPONENTS_TO_DISABLE: Final = "components_to_disable" diff --git a/custom_components/default_config_disabler/helpers.py b/custom_components/default_config_disabler/helpers.py new file mode 100644 index 0000000..2126557 --- /dev/null +++ b/custom_components/default_config_disabler/helpers.py @@ -0,0 +1,57 @@ +"""Helper functions for the Default Config Disabler integration.""" +from __future__ import annotations + +import json +import os +import shutil + +import homeassistant.components as ha_components + +DEFAULT_CONFIG_DIR = os.path.join( + os.path.dirname(os.path.realpath(ha_components.__file__)), "default_config" +) +DEFAULT_CONFIG_MANIFEST = os.path.join(DEFAULT_CONFIG_DIR, "manifest.json") +DEFAULT_CONFIG_MANIFEST_ORIGINAL = os.path.join( + DEFAULT_CONFIG_DIR, "manifest.original.json" +) + + +def backup_original_default_config_manifest() -> bool: + """Backup default_config/manifest.json to manifest.original.json.""" + if not os.path.exists(DEFAULT_CONFIG_MANIFEST_ORIGINAL): + shutil.copyfile(DEFAULT_CONFIG_MANIFEST, DEFAULT_CONFIG_MANIFEST_ORIGINAL) + return True + return False + + +def restore_original_default_config_manifest() -> bool: + """Restore default_config/manifest.json from manifest.original.json.""" + if os.path.exists(DEFAULT_CONFIG_MANIFEST_ORIGINAL): + shutil.copyfile(DEFAULT_CONFIG_MANIFEST_ORIGINAL, DEFAULT_CONFIG_MANIFEST) + os.remove(DEFAULT_CONFIG_MANIFEST_ORIGINAL) + return True + return False + + +def load_original_default_config_manifest() -> dict: + """Load default_config/manifest.original.json.""" + with open(DEFAULT_CONFIG_MANIFEST_ORIGINAL, encoding="utf-8") as f: + return json.load(f) + + +def load_default_config_manifest() -> dict: + """Load default_config/manifest.json.""" + with open(DEFAULT_CONFIG_MANIFEST, encoding="utf-8") as f: + return json.load(f) + + +def save_default_config_manifest(manifest: dict) -> None: + """Save manifest to default_config/manifest.json.""" + with open(DEFAULT_CONFIG_MANIFEST, "w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + + +def get_default_config_components() -> list[str]: + """Return a list of components in the default_config.""" + backup_original_default_config_manifest() + return load_original_default_config_manifest()["dependencies"] diff --git a/custom_components/default_config_disabler/manifest.json b/custom_components/default_config_disabler/manifest.json new file mode 100644 index 0000000..05d5cb8 --- /dev/null +++ b/custom_components/default_config_disabler/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "default_config_disabler", + "name": "Default Config Disabler", + "codeowners": [ + "@tronikos" + ], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/tronikos/default_config_disabler", + "iot_class": "local_push", + "issue_tracker": "https://github.com/tronikos/default_config_disabler/issues", + "requirements": [], + "version": "0.0.0" +} diff --git a/custom_components/default_config_disabler/strings.json b/custom_components/default_config_disabler/strings.json new file mode 100644 index 0000000..4e01900 --- /dev/null +++ b/custom_components/default_config_disabler/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "components_to_disable": "default_config components to disable" + } + } + } + } +} diff --git a/custom_components/default_config_disabler/translations/en.json b/custom_components/default_config_disabler/translations/en.json new file mode 100644 index 0000000..728e4b4 --- /dev/null +++ b/custom_components/default_config_disabler/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start setup?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "components_to_disable": "default_config components to disable" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..6c9a5a2 --- /dev/null +++ b/hacs.json @@ -0,0 +1,7 @@ +{ + "name": "Default Config Disabler", + "homeassistant": "2023.7.0", + "render_readme": true, + "zip_release": true, + "filename": "default_config_disabler.zip" +}