diff --git a/.devcontainer.json b/.devcontainer.json index 95afe1e..5131335 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,35 +1,52 @@ -{ - "name": "custom-components/readme", - "image": "mcr.microsoft.com/devcontainers/python:3.12", - "postCreateCommand": "scripts/setup", - "forwardPorts": [ - 8123 - ], - "portsAttributes": { - "8123": { - "label": "Home Assistant", - "onAutoForward": "notify" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python" - ], - "settings": { - "files.eol": "\n", - "editor.tabSize": 4, - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "editor.formatOnType": false, - "files.trimTrailingWhitespace": true, - "python.formatting.provider": "black", - "python.analysis.typeCheckingMode": "basic", - "python.analysis.autoImportCompletions": true, - "python.defaultInterpreterPath": "/usr/local/bin/python" - } - } - }, - "remoteUser": "vscode", - "features": {} +{ + "name": "custom-components/readme", + "image": "mcr.microsoft.com/devcontainers/python:3.13", + "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": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.13.2" + }, + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "packages": [ + "ffmpeg", + "libturbojpeg0", + "libpcap-dev" + ] + } + } } \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a60ce49 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + ignore: + # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json + - dependency-name: "homeassistant" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..7f3c0e7 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,39 @@ +change-template: "- #$NUMBER $TITLE @$AUTHOR" +sort-direction: ascending +include-labels: + - "beta" + - "breaking-change" + - "bugfix" + - "dependencies" + - "enhancement" + - "feature" + - "refactor" +categories: + - title: "💥 Breaking changes" + label: "breaking-change" + + - title: "🚧 Beta changes" + label: "beta" + collapse-after: 1 + + - title: "✨ New features" + label: "feature" + + - title: "🚀 Performance" + label: "enhancement" + + - title: "🔨 Refactor" + label: "refactor" + + - title: "🐛 Bug fixes" + label: "bugfix" + + - title: "⬆️ Dependency updates" + label: "dependencies" + collapse-after: 1 + +template: | + $CHANGES + + *** + If you are using this integration, please consider enabling [Home Assistant usage analytics](https://www.home-assistant.io/integrations/analytics/#usage-analytics). diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..cfa76c5 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,34 @@ +name: Lint + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + +permissions: {} + +jobs: + ruff: + name: "Ruff" + runs-on: "ubuntu-latest" + steps: + - name: Checkout the repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.13" + cache: "pip" + + - name: Install requirements + run: python3 -m pip install -r requirements.txt + + - name: Lint + run: python3 -m ruff check . + + - name: Format + run: python3 -m ruff format . --check diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..49018d1 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,27 @@ +name: Release Drafter + +on: + push: + branches: + - main + +jobs: + release-drafter: + name: Release Drafter + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4.2.2 + + - name: Get next version + id: version + run: | + echo "version=$(jq .version -r ./custom_components/readme/manifest.json)" >> $GITHUB_OUTPUT + + - name: Run Release Drafter + uses: release-drafter/release-drafter@v6.1.0 + with: + tag: ${{ steps.version.outputs.version }} + name: ${{ steps.version.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c56a6e2..c6ba41b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,61 +1,55 @@ -name: Release Workflow +name: Publish on: release: types: - published + push: + branches: + - main + +concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: true + +permissions: {} jobs: - release: - name: Release + release_zip_file: + name: Publish readme zip file asset runs-on: ubuntu-latest + permissions: + contents: write steps: - name: 📥 Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: 🔢 Get release version - id: version - uses: home-assistant/actions/helpers/version@master + - name: 🛠️ Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.13" - - name: ℹ️ Get integration information - id: information - run: | - name=$(find custom_components/ -type d -maxdepth 1 | tail -n 1 | cut -d "/" -f2) - echo "::set-output name=name::$name" + - name: 🔢 Get version + if: ${{ github.event_name == 'release' }} + id: version + uses: home-assistant/actions/helpers/version@a19f5f4e08ef2786e4604a948f62addd937a6bc9 # master - - name: 🖊️ Set version number + # Pack the readme dir as a zip and upload to the release + - name: 📦 ZIP readme dir run: | - sed -i '/INTEGRATION_VERSION = /c\INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' \ - "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py" - jq '.version = "${{ steps.version.outputs.version }}"' \ - "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" > tmp \ - && mv -f tmp "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" + cd ${{ github.workspace }}/custom_components/readme + zip readme.zip -r ./ - - name: 👀 Validate data - run: | - if ! grep -q 'INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py; then - echo "The version in custom_components/${{ steps.information.outputs.name }}/const.py was not correct" - cat ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py | grep INTEGRATION_VERSION - exit 1 - fi - manifestversion=$(jq -r '.version' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json) - if [ "$manifestversion" != "${{ steps.version.outputs.version }}" ]; then - echo "The version in custom_components/${{ steps.information.outputs.name }}/manifest.json was not correct" - echo "$manifestversion" - exit 1 - fi - - - name: 📦 Create zip file for the integration - run: | - cd "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}" - zip ${{ steps.information.outputs.name }}.zip -r ./ + - name: 📤 Upload zip to action + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: ${{ github.event_name == 'push' }} + with: + name: readme.zip + path: ${{ github.workspace }}/custom_components/readme/readme.zip + retention-days: 7 - - name: 📤 Upload the zip file as a release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 📤 Upload zip to release + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 + if: ${{ github.event_name == 'release' }} with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/${{ steps.information.outputs.name }}.zip" - asset_name: ${{ steps.information.outputs.name }}.zip - asset_content_type: application/zip \ No newline at end of file + files: ${{ github.workspace }}/custom_components/readme/readme.zip diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 7b0a1a5..6f05a26 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -1,37 +1,35 @@ name: Validate - on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" push: branches: - main pull_request: branches: - main - schedule: - - cron: "0 0 * * *" + +permissions: {} jobs: - validate-hassfest: + hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest + name: Hassfest validation runs-on: ubuntu-latest - name: With hassfest steps: - - name: Check out repository - uses: actions/checkout@v2 + - name: Checkout the repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Hassfest validation - uses: "home-assistant/actions/hassfest@master" + - name: Run hassfest validation + uses: home-assistant/actions/hassfest@a19f5f4e08ef2786e4604a948f62addd937a6bc9 # master - validate-hacs: + hacs: # https://github.com/hacs/action + name: HACS validation runs-on: ubuntu-latest - name: With HACS Action steps: - - name: Check out repository - uses: actions/checkout@v2 - - - name: HACS validation - uses: hacs/action@main - with: - category: integration - comment: false - ignore: brands + - name: Run HACS validation + uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0 + with: + category: integration + ignore: brands diff --git a/.gitignore b/.gitignore index fcd512d..dc73b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,18 @@ -## Python temp files +# artifacts __pycache__ +.pytest* +*.egg-info +*/build/* +*/dist/* -## Home Assistant files -.cloud -.storage -.HA_VERSION -blueprints -deps -home-assistant.* -*.db* - -## Integration files -templates -ui-lovelace.yaml \ No newline at end of file + +# misc +.coverage +.vscode +coverage.xml +.ruff_cache + + +# Home Assistant configuration +config/* +!config/configuration.yaml diff --git a/LICENSE b/LICENSE index 8d266b9..e30fde4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Joakim Sørensen @ludeeus +Copyright (c) 2025 Joakim Sørensen @ludeeus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ba119c0..561c656 100644 --- a/README.md +++ b/README.md @@ -19,27 +19,7 @@ with the list of all your installed add-ons and custom components_ 1. Download it with HACS 2. Restart Home Assistant -3. Choose: - - Add `readme:` to your HA configuration. - - In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Generate readme" - -Using your HA configuration directory (folder) as a starting point you should now also have this: - -```text -custom_components/readme/.translations/en.json -custom_components/readme/__init__.py -custom_components/readme/config_flow.py -custom_components/readme/const.py -custom_components/readme/default.j2 -custom_components/readme/manifest.json -custom_components/readme/services.yaml -``` - -## Example configuration.yaml - -```yaml -readme: -``` +3. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Generate readme" ## Warning! diff --git a/config/configuration.yaml b/config/configuration.yaml new file mode 100644 index 0000000..ab2aecf --- /dev/null +++ b/config/configuration.yaml @@ -0,0 +1,14 @@ +default_config: + +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +logger: + default: info + logs: + custom_components.readme: debug + diff --git a/custom_components/readme/__init__.py b/custom_components/readme/__init__.py index 87cadb8..f987825 100644 --- a/custom_components/readme/__init__.py +++ b/custom_components/readme/__init__.py @@ -17,14 +17,16 @@ import voluptuous as vol import yaml from homeassistant import config_entries -from homeassistant.core import callback, HomeAssistant -from homeassistant.components.hassio import is_hassio, get_supervisor_info # type: ignore +from homeassistant.components.hassio import ( # type: ignore + get_supervisor_info, + is_hassio, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.template import AllStates from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration from homeassistant.setup import async_get_loaded_integrations from jinja2 import Template - from .const import DOMAIN, DOMAIN_DATA, LOGGER, STARTUP_MESSAGE CONFIG_SCHEMA = vol.Schema( @@ -91,7 +93,6 @@ def create_initial_files(hass: HomeAssistant): os.mkdir(hass.config.path("templates")) if not os.path.exists(hass.config.path("templates/README.j2")): - copyfile( hass.config.path("custom_components/readme/default.j2"), hass.config.path("templates/README.j2"), @@ -120,7 +121,7 @@ async def read_file(hass: HomeAssistant, path: str) -> Any: """Read a file.""" def read(): - with open(hass.config.path(path), "r") as open_file: + with open(hass.config.path(path)) as open_file: return open_file.read() return await hass.async_add_executor_job(read) @@ -202,7 +203,7 @@ def get_hacs_components(hass: HomeAssistant): @callback -def get_ha_installed_addons(hass: HomeAssistant) -> List[Dict[str, Any]]: +def get_ha_installed_addons(hass: HomeAssistant) -> list[dict[str, Any]]: if is_hassio(hass): return [] supervisor_info = get_supervisor_info(hass) @@ -232,14 +233,14 @@ def get_repository_name(repository) -> str: async def get_custom_integrations(hass: HomeAssistant): """Return a list with custom integration info.""" custom_integrations = [] - configured_integrations: List[Integration | IntegrationNotFound | BaseException] = ( - await asyncio.gather( - *[ - async_get_integration(hass, domain) - for domain in async_get_loaded_integrations(hass) - ], - return_exceptions=True, - ) + configured_integrations: list[ + Integration | IntegrationNotFound | BaseException + ] = await asyncio.gather( + *[ + async_get_integration(hass, domain) + for domain in async_get_loaded_integrations(hass) + ], + return_exceptions=True, ) for integration in configured_integrations: diff --git a/custom_components/readme/config_flow.py b/custom_components/readme/config_flow.py index 613c200..b0cb081 100644 --- a/custom_components/readme/config_flow.py +++ b/custom_components/readme/config_flow.py @@ -19,9 +19,7 @@ def __init__(self): """Initialize.""" self._errors = {} - async def async_step_user( - self, user_input={} - ): # pylint: disable=dangerous-default-value + async def async_step_user(self, user_input={}): # pylint: disable=dangerous-default-value """Handle a flow initialized by the user.""" self._errors = {} if self._async_current_entries(): @@ -36,13 +34,11 @@ async def async_step_user( async def _show_config_form(self, user_input): """Show the configuration form to edit data.""" - # Defaults convert = False - if user_input is not None: - if "convert" in user_input: - convert = user_input["convert"] + if user_input is not None and "convert" in user_input: + convert = user_input["convert"] data_schema = OrderedDict() data_schema[vol.Required("convert", default=convert)] = bool @@ -51,7 +47,8 @@ async def _show_config_form(self, user_input): ) async def async_step_import(self, user_input): # pylint: disable=unused-argument - """Import a config entry. + """ + Import a config entry. Special type of import, we're not actually going to store any data. Instead, we're going to rely on the values that are in config file. """ diff --git a/requirements.txt b/requirements.txt index 09d69a9..0687439 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -black -colorlog -homeassistant \ No newline at end of file +colorlog==6.9.0 +homeassistant==2025.5.1 +pip>=21.3.1 +ruff==0.11.9 \ No newline at end of file diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..04cf0da --- /dev/null +++ b/ruff.toml @@ -0,0 +1,46 @@ +target-version = "py313" + +[lint] +select = [ + "ALL", +] + +ignore = [ + "ANN001", + "ANN201", + "ANN202", + "ANN204", + "ANN401", + "ARG001", + "ARG002", + "B006", + "BLE001", + "COM812", + "D101", + "D102", + "D103", + "D203", + "D205", + "D212", + "D400", + "D415", + "F401", + "FBT002", + "FBT003", + "ISC001", + "PGH003", + "PTH102", + "PTH110", + "PTH123", + "UP008", + "UP035", +] + +[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/scripts/develop b/scripts/develop index dca3958..fda629c 100755 --- a/scripts/develop +++ b/scripts/develop @@ -1,5 +1,11 @@ #!/usr/bin/env bash + set -e cd "$(dirname "$0")/.." -hass -c . \ No newline at end of file + +# Set the python path to include our custom_components directory +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/scripts/setversion b/scripts/setversion new file mode 100755 index 0000000..659234b --- /dev/null +++ b/scripts/setversion @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +NEW_VERSION=$1 + +jq --arg version "$NEW_VERSION" '.version = $version' ./custom_components/readme/manifest.json > _manifest_tmp.json +mv _manifest_tmp.json ./custom_components/readme/manifest.json \ No newline at end of file