diff --git a/.bumpversion.toml b/.bumpversion.toml index cfb5766..508b6f4 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,12 +1,19 @@ [tool.bumpversion] -current_version = "1.4.2" +current_version = "1.5.2" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] -search = "{current_version}" -replace = "{new_version}" -regex = false +commit = true +tag = true +tag_name = "v{new_version}" +commit_args = "--no-verify" +message = "chore: bump version to {new_version}" [[tool.bumpversion.files]] filename = "custom_components/simple_pid_controller/manifest.json" search = '"version": "{current_version}"' replace = '"version": "{new_version}"' + +[[tool.bumpversion.files]] +filename = "setup.cfg" +search = "current_version = {current_version}" +replace = "current_version = {new_version}" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6ad5115 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,68 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! Please fill in the form as completely as possible. + + - type: input + id: version + attributes: + label: Version + description: Which version are you using? + placeholder: "e.g. 1.2.3" + validations: + required: true + + - type: input + id: ha_version + attributes: + label: Home Assistant version (if applicable) + placeholder: "e.g. 2025.2.0" + + - type: textarea + id: description + attributes: + label: Bug description + description: What goes wrong? Be as specific as possible. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: How can we reproduce this? + placeholder: | + 1. Go to ... + 2. Click on ... + 3. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What should happen instead? + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs / Error messages + description: Paste relevant logs or error messages (Home Assistant log, browser console, etc.) + render: text + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched existing issues and this is not a duplicate + required: true + - label: I am using the latest version + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f48bfdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,45 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for your suggestion! Please describe your request as clearly as possible. + + - type: textarea + id: problem + attributes: + label: What problem does this solve? + description: Describe the problem or need that motivates this request. + placeholder: "As a user I want to ... so that ..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How would you implement this? + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Have you considered other solutions? + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context, screenshots, or examples. + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched existing issues and this is not a duplicate + required: true diff --git a/.github/labeler.yml b/.github/labeler.yml index ba646e2..db61a02 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,23 @@ bug: - - "error" - - "fail" -feature: - - "feature request" - - "enhancement" + - '(?i)(bug|error|exception|traceback|crash|not working|broken|failing|AttributeError|ValueError|TypeError|KeyError|ImportError|does not work|stopped working)' + +enhancement: + - '(?i)(feature|enhancement|feature request|add support|support for|request|wish|suggestion|could you|can you add)' + +question: + - '(?i)(question|how to|how do|what is|help|explain|why|where|where can|confused|understand)' + documentation: - - "docs" - - "documentation" -maintenance: - - "dependabot" + - '(?i)(docs|documentation|readme|example|wiki|manual|how to configure|guide)' + +configuration: + - '(?i)(config|setup|install|yaml|options|settings|configure|option|parameter)' + +performance: + - '(?i)(slow|performance|memory|cpu|hang|freeze|timeout|lagging|delayed)' + +"home assistant": + - '(?i)(home assistant|hass|HA \d+\.\d+|homeassistant|core \d+\.\d+)' +dependencies: + - '(?i)(dependency|requirement|package|pip|version incompatible|requires|needs version)' diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..14d1f69 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,52 @@ +# GitHub Labels β€” synced via sync-labels.yml workflow +- name: bug + color: "d73a4a" + description: "Something isn't working as expected" + +- name: enhancement + color: "a2eeef" + description: "New feature or improvement" + +- name: question + color: "d876e3" + description: "More information needed" + +- name: documentation + color: "0075ca" + description: "Improvement or addition to documentation" + +- name: configuration + color: "e4e669" + description: "Configuration or setup related" + +- name: performance + color: "f9d0c4" + description: "Performance or resource usage" + +- name: dependencies + color: "0366d6" + description: "Update dependencies" + +- name: duplicate + color: "cfd3d7" + description: "This issue already exists" + +- name: wontfix + color: "ffffff" + description: "Will not be fixed" + +- name: "good first issue" + color: "7057ff" + description: "Good for new contributors" + +- name: "help wanted" + color: "008672" + description: "Extra attention or help wanted" + +- name: stale + color: "ededed" + description: "No recent activity" + +- name: "breaking change" + color: "e11d48" + description: "Contains a breaking change" diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 0000000..59972b2 --- /dev/null +++ b/.github/pr-labeler.yml @@ -0,0 +1,23 @@ +bug: + - changed-files: + - any-glob-to-any-file: [] # no file-based bug detection; use issue labels or manual + +enhancement: + - changed-files: + - any-glob-to-any-file: + - custom_components/simple_pid_controller/** + +documentation: + - changed-files: + - any-glob-to-any-file: + - "*.md" + - docs/** + - custom_components/simple_pid_controller/translations/** + - custom_components/simple_pid_controller/strings.json + +dependencies: + - changed-files: + - any-glob-to-any-file: + - requirements*.txt + - custom_components/simple_pid_controller/manifest.json + - setup.cfg diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 909eebd..c5d5bc4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,33 +1,22 @@ -## πŸ“ What’s Changed? +## Description - -- ... + -## πŸ” Why is this Change Needed? +## Type of change - -- ... +- [ ] `fix:` Bug fix (patch version bump) +- [ ] `feat:` New feature (minor version bump) +- [ ] `feat!:` / `BREAKING CHANGE:` Breaking change (major version bump) +- [ ] `chore:` / `docs:` / `ci:` Maintenance or documentation (no version bump) -## πŸ§ͺ How Was This Tested? +## Checklist - -- [ ] Added or updated unit tests -- [ ] Manually tested in development environment -- [ ] CI/CD pipeline passed successfully - -## βœ… Checklist - -- [ ] Code follows the style guide -- [ ] All tests are passing +- [ ] Commit title follows [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.) +- [ ] Tests added or updated where applicable - [ ] Documentation updated if needed -- [ ] No breaking changes (or clearly documented) - -## πŸ“Έ Screenshots / Logs (Optional) - - -_(e.g., Home Assistant UI, terminal output, etc.)_ +- [ ] CI is green +- [ ] PR targets the `dev` branch (not `main`, unless this is a hotfix) -## πŸ“Ž Additional Notes +## Screenshots / Logs (optional) - -- ... + diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 0000000..90d3c4b --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,51 @@ +name: Bump Version + +on: + workflow_dispatch: + inputs: + bump: + description: "Version bump type" + type: choice + options: [patch, minor, major] + default: patch + required: true + +permissions: + contents: write + +jobs: + bump: + name: Bump ${{ inputs.bump }} version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install bump-my-version + run: pip install bump-my-version + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump version + run: bump-my-version bump ${{ inputs.bump }} + + - name: Push commit and tag + run: | + git push origin main + git push origin --tags + + - name: Show new version + run: | + NEW_TAG=$(git describe --tags --abbrev=0) + echo "Version bumped to $NEW_TAG" + echo "### Version bumped to $NEW_TAG" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..997f83f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + branches: ["dev", "feature/**", "fix/**", "hotfix/**"] + tags-ignore: ["**"] + pull_request: + branches: [dev, main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint & pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files + + test: + name: Test (Python 3.13) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + cache: pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run pytest + run: pytest --cov=custom_components --cov-report=xml -q + + - name: Upload coverage + uses: codecov/codecov-action@v6 + with: + files: ./coverage.xml + fail_ci_if_error: false + + hacs: + name: HACS validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: hacs/action@main + with: + category: integration + + hassfest: + name: Home Assistant hassfest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/hacs.yml b/.github/workflows/hacs.yml deleted file mode 100644 index bb82ed0..0000000 --- a/.github/workflows/hacs.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: HACS - -on: - push: - branches: - - main - release: - types: [published] - schedule: - - cron: "0 0 * * *" - -permissions: - contents: read - -jobs: - validate-hacs: - name: "HACS" - runs-on: ubuntu-latest - steps: - - name: HACS Validation - uses: hacs/action@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - category: "integration" - diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml deleted file mode 100644 index e261734..0000000 --- a/.github/workflows/hassfest.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Hassfest - -on: - schedule: - - cron: "0 0 * * *" - push: - branches: - - main - - dev - pull_request: - branches: - - dev - -permissions: - contents: read - -jobs: - hassfest: - name: "Hassfest" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Run hassfest - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml index ae6d9ec..b2f2761 100644 --- a/.github/workflows/label-issues.yml +++ b/.github/workflows/label-issues.yml @@ -1,19 +1,20 @@ name: Label issues + on: issues: - types: - - reopened - - opened - - edited + types: [opened, reopened, edited] + +permissions: + issues: write + jobs: label_issues: - name: "Automatically Label Issues" + name: Automatically label issues runs-on: ubuntu-latest - permissions: - issues: write steps: - uses: github/issue-labeler@v3.4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml - + enable-versioned-regex: 0 + include-title: 1 diff --git a/.github/workflows/label-prs.yml b/.github/workflows/label-prs.yml new file mode 100644 index 0000000..54030ba --- /dev/null +++ b/.github/workflows/label-prs.yml @@ -0,0 +1,33 @@ +name: Label PRs + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + name: Automatically label PRs by changed files + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/pr-labeler.yml + + - name: Label bug based on PR title + uses: actions/github-script@v9 + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + if (/^fix[\s(:!]/.test(title)) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['bug'], + }); + } diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml deleted file mode 100644 index 9dfe140..0000000 --- a/.github/workflows/precommit.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Run Code Quality Check - -permissions: - contents: write - -on: - push: - branches: - - dev - - main - pull_request: - branches: - - dev - - main - workflow_dispatch: - -jobs: - test: - name: "Code Quality Check" - env: - PYTHONPATH: . - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: 'pip' # caching pip dependencies - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pre-commit - - - name: Run pre-commit hooks - run: | - # Pre-commit exits with code 1 when it makes changes. We don't want - # the workflow to fail in that case, so ignore the exit code. - pre-commit run --all-files || true - - - name: Commit and push changes - if: success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - echo "No changes to commit" - else - git commit -m "chore: apply pre-commit fixes" - BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" - git push origin "HEAD:${BRANCH}" - fi diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml deleted file mode 100644 index cc17793..0000000 --- a/.github/workflows/pytest.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Run Pytest - -on: - push: - branches: - - dev - - main - pull_request: - branches: - - dev - - main - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - name: "Pytest" - env: - PYTHONPATH: . - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: 'pip' # caching pip dependencies - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run tests - run: pytest --maxfail=1 --disable-warnings -q diff --git a/.github/workflows/release-versioning.yml b/.github/workflows/release-versioning.yml deleted file mode 100644 index f5e97d9..0000000 --- a/.github/workflows/release-versioning.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release Versioning - -on: - # Trigger bij push van een tag vX.Y.Z - push: - tags: - - 'v*.*.*' - # Trigger zodra je in de GitHub UI een Release publiceert - release: - types: [published] - -permissions: - contents: write - -jobs: - bump-release: - # Zorg dat we alleen in deze job de tag-push afhandelen - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - ref: main - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install bump2version - run: pip install bump2version - - - name: Bump version in manifest.json - # Gebruik --no-tag zodat we alleen het bestand bumpen voor de release - run: bump2version patch --allow-dirty --no-tag - - - name: Commit & Push bumped files - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add custom_components/simple_pid_controller/manifest.json setup.cfg - git commit -m "chore: bump to next version after release ${{ github.ref_name }}" - git push origin HEAD:main - - publish-release: - # Deze job draait na het aanmaken van de GitHub Release (event β€˜published’) - if: github.event_name == 'release' - needs: bump-release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Create GitHub Release from tag - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.event.release.tag_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75ea114..f2a9a0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,32 +1,77 @@ -name: Create release zip +name: Create Release on: - release: - types: [published] + push: + tags: ["v*.*.*"] permissions: contents: write - + jobs: - build-and-upload: + release: + name: Build ZIP and create GitHub Release runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - - name: Get version + - name: Get version from tag id: version - uses: home-assistant/actions/helpers/version@master + run: | + VERSION=${GITHUB_REF_NAME#v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Update version in manifest.json + run: | + sed -i 's/"version": "[^"]*"/"version": "${{ steps.version.outputs.version }}"/' \ + custom_components/simple_pid_controller/manifest.json - - name: Patch manifest and zip + - name: Create release ZIP run: | - sed -i 's/v0.0.0/${{ steps.version.outputs.version }}/' custom_components/simple_pid_controller/manifest.json cd custom_components/simple_pid_controller/ zip ../../simple_pid_controller.zip -r ./ - - uses: svenstaro/upload-release-action@master + + - name: Generate changelog + id: changelog + uses: mikepenz/release-changelog-builder-action@v6 + with: + commitMode: true + configuration: | + { + "label_extractor": [ + { "pattern": "^(feat|feature|add)(\\(.+\\))?!?[:\\s]", "target": "feature" }, + { "pattern": "^(fix|bug|bugfix|hotfix)(\\(.+\\))?!?[:\\s]", "target": "bug" }, + { "pattern": "^(docs?|documentation)(\\(.+\\))?[:\\s]", "target": "documentation" }, + { "pattern": "^(ci|github.?actions?)(\\(.+\\))?[:\\s]", "target": "ci" }, + { "pattern": "^(chore|refactor|test|perf|style|build|bump)(\\(.+\\))?[:\\s]", "target": "chore" } + ], + "categories": [ + { "title": "## ✨ New features", "labels": ["feature", "enhancement"] }, + { "title": "## πŸ› Bug fixes", "labels": ["bug", "fix"] }, + { "title": "## πŸ“š Documentation", "labels": ["documentation"] }, + { "title": "## πŸ”§ Other", "labels": ["chore"] }, + { "title": "## πŸ‘· CI", "labels": ["ci"] }, + { "title": "## πŸ“¦ Uncategorized", "labels": [] } + ], + "ignore_labels": ["dependabot"], + "template": "#{{CHANGELOG}}\n\n**Full changelog**: #{{RELEASE_DIFF}}" + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ./simple_pid_controller.zip - asset_name: simple_pid_controller.zip - tag: ${{ github.ref }} - overwrite: true + files: simple_pid_controller.zip + body: | + ${{ steps.changelog.outputs.changelog }} + + --- + + ## Installation via HACS + Install or update via HACS β€” the new version will be offered automatically. + + ## Manual installation + Download `simple_pid_controller.zip`, extract and copy `simple_pid_controller/` to your `custom_components/` directory. + generate_release_notes: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index edbe8de..ca00c61 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,7 +2,6 @@ name: Mark & close stale issues/PRs on: schedule: - # Dagelijks om 02:17 UTC - cron: "17 2 * * *" workflow_dispatch: @@ -16,42 +15,37 @@ jobs: steps: - uses: actions/stale@v10 with: - # ALGEMEEN repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 200 - days-before-stale: 7 # Issues worden 'stale' na 60 dagen inactiviteit - days-before-close: 14 # en gesloten 14 dagen later + days-before-stale: 60 + days-before-close: 14 stale-issue-label: "stale" stale-pr-label: "stale" exempt-issue-labels: "pinned,security,backlog,never-stale" exempt-pr-labels: "work-in-progress,never-stale" - exempt-all-milestones: true # Items met milestone overslaan - exempt-assignees: "" # Vul evt. gebruikers in, kommagescheiden + exempt-all-milestones: true remove-stale-when-updated: true - ignore-updates: false # Als iemand reageert of labelt, wordt 'stale' verwijderd + ignore-updates: false ascending: false - # ISSUES stale-issue-message: | - πŸ’€ Deze issue heeft 60 dagen geen activiteit gehad en is gemarkeerd als **stale**. - Als er binnen 14 dagen geen nieuwe activiteit is, wordt deze automatisch gesloten. - Voeg svp een update toe of label met `never-stale` om dit te voorkomen. + This issue has been inactive for 60 days and is marked as **stale**. + If there is no new activity within 14 days it will be closed automatically. + Please add an update or label with `never-stale` to keep it open. close-issue-message: | - πŸ”’ Deze issue is automatisch gesloten wegens inactiviteit. - Als dit nog steeds actueel is, heropen of maak een nieuwe issue met de laatste context. Bedankt! + This issue was automatically closed due to inactivity. + If still relevant, please reopen or create a new issue with updated context. - # PULL REQUESTS - days-before-pr-stale: 30 # PRs iets sneller stale + days-before-pr-stale: 30 days-before-pr-close: 10 stale-pr-message: | - πŸ’€ Deze pull request is 30 dagen inactief en is gemarkeerd als **stale**. - Reageer of push nieuwe commits binnen 10 dagen om sluiten te voorkomen. - Label met `never-stale` om uit te sluiten. + This pull request has been inactive for 30 days and is marked as **stale**. + Please respond or push new commits within 10 days to prevent closing. + Label with `never-stale` to exclude. close-pr-message: | - πŸ”’ Deze pull request is automatisch gesloten wegens inactiviteit. - Heropen gerust wanneer je verder wilt gaan. + This pull request was automatically closed due to inactivity. + Feel free to reopen when you are ready to continue. - # FILTERS (optioneel; laat leeg om alles te laten meedraaien) - only-labels: "" # Bv. "triage" om alleen issues met dat label te targeten - any-of-labels: "" # Bv. "question,help wanted" - exempt-draft-pr: true # Draft PRs overslaan + only-labels: "" + any-of-labels: "" + exempt-draft-pr: true diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..edb12b1 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,23 @@ +name: Sync Labels + +on: + workflow_dispatch: + push: + paths: [".github/labels.yml"] + branches: [main] + +permissions: + issues: write + +jobs: + sync: + name: Sync repository labels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: EndBug/label-sync@v2 + with: + config-file: .github/labels.yml + delete-other-labels: false + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/assets/dark_icon.png b/custom_components/simple_pid_controller/brand/dark_icon.png similarity index 100% rename from assets/dark_icon.png rename to custom_components/simple_pid_controller/brand/dark_icon.png diff --git a/assets/dark_icon@2x.png b/custom_components/simple_pid_controller/brand/dark_icon@2x.png similarity index 100% rename from assets/dark_icon@2x.png rename to custom_components/simple_pid_controller/brand/dark_icon@2x.png diff --git a/assets/dark_logo.png b/custom_components/simple_pid_controller/brand/dark_logo.png similarity index 100% rename from assets/dark_logo.png rename to custom_components/simple_pid_controller/brand/dark_logo.png diff --git a/assets/dark_logo@2x.png b/custom_components/simple_pid_controller/brand/dark_logo@2x.png similarity index 100% rename from assets/dark_logo@2x.png rename to custom_components/simple_pid_controller/brand/dark_logo@2x.png diff --git a/assets/icon.png b/custom_components/simple_pid_controller/brand/icon.png similarity index 100% rename from assets/icon.png rename to custom_components/simple_pid_controller/brand/icon.png diff --git a/assets/icon@2x.png b/custom_components/simple_pid_controller/brand/icon@2x.png similarity index 100% rename from assets/icon@2x.png rename to custom_components/simple_pid_controller/brand/icon@2x.png diff --git a/assets/logo.png b/custom_components/simple_pid_controller/brand/logo.png similarity index 100% rename from assets/logo.png rename to custom_components/simple_pid_controller/brand/logo.png diff --git a/assets/logo@2x.png b/custom_components/simple_pid_controller/brand/logo@2x.png similarity index 100% rename from assets/logo@2x.png rename to custom_components/simple_pid_controller/brand/logo@2x.png diff --git a/custom_components/simple_pid_controller/config_flow.py b/custom_components/simple_pid_controller/config_flow.py index b64a162..7e479d5 100644 --- a/custom_components/simple_pid_controller/config_flow.py +++ b/custom_components/simple_pid_controller/config_flow.py @@ -25,10 +25,12 @@ CONF_INPUT_RANGE_MAX, CONF_OUTPUT_RANGE_MIN, CONF_OUTPUT_RANGE_MAX, + CONF_STEP_PREFIX, DEFAULT_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MAX, + DEFAULT_STEPS, ) _LOGGER = logging.getLogger(__name__) @@ -146,6 +148,18 @@ async def async_step_init( CONF_OUTPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MAX ) + step_fields = { + vol.Optional( + f"{CONF_STEP_PREFIX}{key}", + default=self.config_entry.options.get( + f"{CONF_STEP_PREFIX}{key}", DEFAULT_STEPS[key] + ), + ): selector( + {"number": {"min": 0.0001, "max": 100.0, "step": 0.001, "mode": "box"}} + ) + for key in DEFAULT_STEPS + } + options_schema = vol.Schema( { vol.Required( @@ -168,6 +182,7 @@ async def async_step_init( CONF_OUTPUT_RANGE_MAX, default=current_output_max, ): vol.Coerce(float), + **step_fields, } ) diff --git a/custom_components/simple_pid_controller/const.py b/custom_components/simple_pid_controller/const.py index 592b1b5..d4a9b8e 100644 --- a/custom_components/simple_pid_controller/const.py +++ b/custom_components/simple_pid_controller/const.py @@ -16,3 +16,16 @@ DEFAULT_INPUT_RANGE_MAX = 100.0 DEFAULT_OUTPUT_RANGE_MIN = 0.0 DEFAULT_OUTPUT_RANGE_MAX = 100.0 + +CONF_STEP_PREFIX = "step_" + +DEFAULT_STEPS: dict[str, float] = { + "kp": 0.0001, + "ki": 0.0001, + "kd": 0.0001, + "sample_time": 0.01, + "setpoint": 0.01, + "output_min": 1.0, + "output_max": 1.0, + "starting_output": 1.0, +} diff --git a/custom_components/simple_pid_controller/manifest.json b/custom_components/simple_pid_controller/manifest.json index 1cc62c8..c0063c8 100644 --- a/custom_components/simple_pid_controller/manifest.json +++ b/custom_components/simple_pid_controller/manifest.json @@ -1,7 +1,9 @@ { "domain": "simple_pid_controller", "name": "Simple PID Controller", - "codeowners": ["@bvweerd"], + "codeowners": [ + "@bvweerd" + ], "config_flow": true, "dependencies": [], "documentation": "https://www.github.com/bvweerd/simple_pid_controller", @@ -9,8 +11,10 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/bvweerd/simple_pid_controller/issues", "quality_scale": "silver", - "requirements": ["simple-pid==2.0.1"], + "requirements": [ + "simple-pid==2.0.1" + ], "ssdp": [], - "version": "1.4.2", + "version": "1.5.2", "zeroconf": [] } diff --git a/custom_components/simple_pid_controller/number.py b/custom_components/simple_pid_controller/number.py index b429ceb..b8fc4f3 100644 --- a/custom_components/simple_pid_controller/number.py +++ b/custom_components/simple_pid_controller/number.py @@ -16,10 +16,12 @@ CONF_INPUT_RANGE_MAX, CONF_OUTPUT_RANGE_MIN, CONF_OUTPUT_RANGE_MAX, + CONF_STEP_PREFIX, DEFAULT_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MAX, + DEFAULT_STEPS, ) # Coordinator is used to centralize the data updates @@ -129,7 +131,9 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None: self._attr_native_unit_of_measurement = desc["unit"] self._attr_native_min_value = desc["min"] self._attr_native_max_value = desc["max"] - self._attr_native_step = desc["step"] + self._attr_native_step = (entry.options or {}).get( + f"{CONF_STEP_PREFIX}{desc['key']}", DEFAULT_STEPS[desc["key"]] + ) self._attr_native_value = desc["default"] self._attr_entity_category = desc["entity_category"] @@ -162,7 +166,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None: self._attr_icon = "mdi:ray-vertex" self._attr_mode = "box" self._attr_native_unit_of_measurement = desc["unit"] - self._attr_native_step = desc["step"] self._attr_native_value = desc["default"] self._attr_entity_category = desc["entity_category"] self._key = desc["key"] @@ -208,7 +211,10 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, desc: dict) -> None: self._attr_native_min_value = min_val self._attr_native_max_value = max_val - self._attr_native_step = desc.get("step", 1.0) + self._attr_native_step = opts.get( + f"{CONF_STEP_PREFIX}{self._key}", + DEFAULT_STEPS.get(self._key, desc.get("step", 1.0)), + ) # Initialize current value if self._key == "setpoint": diff --git a/custom_components/simple_pid_controller/strings.json b/custom_components/simple_pid_controller/strings.json index 6bdd0a9..93d3b4e 100644 --- a/custom_components/simple_pid_controller/strings.json +++ b/custom_components/simple_pid_controller/strings.json @@ -28,7 +28,25 @@ "input_range_min": "Minimum Input Range", "input_range_max": "Maximum Input Range", "output_range_min": "Minimum Output Range", - "output_range_max": "Maximum Output Range" + "output_range_max": "Maximum Output Range", + "step_kp": "Kp Step Size", + "step_ki": "Ki Step Size", + "step_kd": "Kd Step Size", + "step_sample_time": "Sample Time Step Size", + "step_setpoint": "Setpoint Step Size", + "step_output_min": "Output Min Step Size", + "step_output_max": "Output Max Step Size", + "step_starting_output": "Startup Value Step Size" + }, + "data_description": { + "step_kp": "Increment size for the Kp parameter.", + "step_ki": "Increment size for the Ki parameter.", + "step_kd": "Increment size for the Kd parameter.", + "step_sample_time": "Increment size for the sample time parameter.", + "step_setpoint": "Increment size for the setpoint picker.", + "step_output_min": "Increment size for the output minimum.", + "step_output_max": "Increment size for the output maximum.", + "step_starting_output": "Increment size for the startup value." } } } diff --git a/custom_components/simple_pid_controller/translations/en.json b/custom_components/simple_pid_controller/translations/en.json index 6e12af2..0a5d4b2 100644 --- a/custom_components/simple_pid_controller/translations/en.json +++ b/custom_components/simple_pid_controller/translations/en.json @@ -34,7 +34,25 @@ "input_range_min": "Minimum Input Range", "input_range_max": "Maximum Input Range", "output_range_min": "Minimum Output Range", - "output_range_max": "Maximum Output Range" + "output_range_max": "Maximum Output Range", + "step_kp": "Kp Step Size", + "step_ki": "Ki Step Size", + "step_kd": "Kd Step Size", + "step_sample_time": "Sample Time Step Size", + "step_setpoint": "Setpoint Step Size", + "step_output_min": "Output Min Step Size", + "step_output_max": "Output Max Step Size", + "step_starting_output": "Startup Value Step Size" + }, + "data_description": { + "step_kp": "Increment size for the Kp parameter.", + "step_ki": "Increment size for the Ki parameter.", + "step_kd": "Increment size for the Kd parameter.", + "step_sample_time": "Increment size for the sample time parameter.", + "step_setpoint": "Increment size for the setpoint picker.", + "step_output_min": "Increment size for the output minimum.", + "step_output_max": "Increment size for the output maximum.", + "step_starting_output": "Increment size for the startup value." } } }, @@ -42,7 +60,7 @@ "range_min_max": "Minimum must be lower than maximum." } }, - "entity": { + "entity": { "number": { "kp": { "name": "Kp" @@ -75,23 +93,23 @@ "current_value": { "name": "Current Value" } - } - } - , - "services": { - "set_output": { - "name": "Set PID output", - "description": "Set or reset the PID controller output.", - "fields": { - "preset": { - "name": "Preset", - "description": "Use a preset output: zero_start, last_known_value or startup_value." - }, - "value": { - "name": "Value", - "description": "Manual output value between output_min and output_max." - } - } - } - } -} + } + } + , + "services": { + "set_output": { + "name": "Set PID output", + "description": "Set or reset the PID controller output.", + "fields": { + "preset": { + "name": "Preset", + "description": "Use a preset output: zero_start, last_known_value or startup_value." + }, + "value": { + "name": "Value", + "description": "Manual output value between output_min and output_max." + } + } + } + } +} diff --git a/custom_components/simple_pid_controller/translations/nl.json b/custom_components/simple_pid_controller/translations/nl.json index 0d73623..f812e1c 100644 --- a/custom_components/simple_pid_controller/translations/nl.json +++ b/custom_components/simple_pid_controller/translations/nl.json @@ -33,7 +33,25 @@ "input_range_min": "Minimum Input Bereik", "input_range_max": "Maximum Input Bereik", "output_range_min": "Minimum Output Bereik", - "output_range_max": "Maximum Output Bereik" + "output_range_max": "Maximum Output Bereik", + "step_kp": "Kp Stapgrootte", + "step_ki": "Ki Stapgrootte", + "step_kd": "Kd Stapgrootte", + "step_sample_time": "Steektijd Stapgrootte", + "step_setpoint": "Stapgrootte doelwaarde", + "step_output_min": "Stapgrootte min. uitvoer", + "step_output_max": "Stapgrootte max. uitvoer", + "step_starting_output": "Stapgrootte startwaarde" + }, + "data_description": { + "step_kp": "Stapgrootte voor de Kp-parameter.", + "step_ki": "Stapgrootte voor de Ki-parameter.", + "step_kd": "Stapgrootte voor de Kd-parameter.", + "step_sample_time": "Stapgrootte voor de steektijd.", + "step_setpoint": "Stapgrootte voor de setpoint-kiezer.", + "step_output_min": "Stapgrootte voor de minimale uitvoer.", + "step_output_max": "Stapgrootte voor de maximale uitvoer.", + "step_starting_output": "Stapgrootte voor de startwaarde." } } }, diff --git a/setup.cfg b/setup.cfg index f2a8522..dfd4e5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.3 +current_version = 1.5.2 [coverage:run] source = diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 1d28955..5218184 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,9 +1,11 @@ import pytest + from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import FlowResultType, InvalidData from custom_components.simple_pid_controller.const import ( + CONF_STEP_PREFIX, DOMAIN, CONF_NAME, CONF_SENSOR_ENTITY_ID, @@ -15,6 +17,7 @@ DEFAULT_INPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MAX, + DEFAULT_STEPS, ) from custom_components.simple_pid_controller.config_flow import ( PIDControllerFlowHandler, @@ -184,7 +187,78 @@ async def test_options_flow(hass, config_entry, new_options, expected_errors): assert result2.get("errors") == expected_errors else: assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2.get("data") == new_options + # Step defaults are added by voluptuous; check submitted fields are present + assert new_options.items() <= result2.get("data", {}).items() + + +async def test_options_flow_with_custom_steps(hass, config_entry): + """Test that step options round-trip correctly through the options flow.""" + custom_steps = {f"{CONF_STEP_PREFIX}{key}": 0.5 for key in DEFAULT_STEPS} + new_options = { + CONF_SENSOR_ENTITY_ID: "sensor.new", + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, + **custom_steps, + } + + init_result = await hass.config_entries.options.async_init(config_entry.entry_id) + result2 = await hass.config_entries.options.async_configure( + init_result["flow_id"], user_input=new_options + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + for key, val in custom_steps.items(): + assert result2["data"][key] == val + + +_BASE_OPTIONS = { + CONF_SENSOR_ENTITY_ID: "sensor.new", + CONF_INPUT_RANGE_MIN: 1.0, + CONF_INPUT_RANGE_MAX: 10.0, + CONF_OUTPUT_RANGE_MIN: 1.0, + CONF_OUTPUT_RANGE_MAX: 10.0, +} + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("invalid_step", [0.0, -1.0, -0.0001]) +async def test_options_flow_step_below_min_rejected(hass, config_entry, invalid_step): + """Test that a step value below the selector minimum raises InvalidData.""" + init_result = await hass.config_entries.options.async_init(config_entry.entry_id) + with pytest.raises(InvalidData): + await hass.config_entries.options.async_configure( + init_result["flow_id"], + user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}kp": invalid_step}, + ) + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("invalid_step", [100.001, 200.0, 1000.0]) +async def test_options_flow_step_above_max_rejected(hass, config_entry, invalid_step): + """Test that a step value above the selector maximum raises InvalidData.""" + init_result = await hass.config_entries.options.async_init(config_entry.entry_id) + with pytest.raises(InvalidData): + await hass.config_entries.options.async_configure( + init_result["flow_id"], + user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}setpoint": invalid_step}, + ) + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("boundary_step", [0.0001, 100.0]) +async def test_options_flow_step_boundary_values_accepted( + hass, config_entry, boundary_step +): + """Test that step values at the exact selector boundaries are accepted.""" + init_result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + init_result["flow_id"], + user_input={**_BASE_OPTIONS, f"{CONF_STEP_PREFIX}kp": boundary_step}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][f"{CONF_STEP_PREFIX}kp"] == boundary_step async def test_user_flow_duplicate_abort(hass): diff --git a/tests/test_number.py b/tests/test_number.py index b0d942a..91c1328 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -7,10 +7,13 @@ ControlParameterNumber, ) from custom_components.simple_pid_controller.const import ( + CONF_STEP_PREFIX, DEFAULT_INPUT_RANGE_MIN, DEFAULT_INPUT_RANGE_MAX, DEFAULT_OUTPUT_RANGE_MIN, DEFAULT_OUTPUT_RANGE_MAX, + DEFAULT_STEPS, + DOMAIN, ) @@ -163,3 +166,59 @@ async def test_controlparameter_number_unexpected_key( assert f"Unknown PID key '{invalid_key}'. Using default values:" in caplog.text assert num._attr_native_min_value == expected_min assert num._attr_native_max_value == expected_max + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("desc", PID_NUMBER_ENTITIES) +async def test_pid_number_step_from_options(hass, config_entry, desc): + """Test that PIDParameterNumber reads step from CONF_STEP_PREFIX+key in options.""" + from types import SimpleNamespace + from pytest_homeassistant_custom_component.common import MockConfigEntry + + custom_step = 0.5 + entry = MockConfigEntry( + domain=DOMAIN, + entry_id=f"step_test_{desc['key']}", + data=config_entry.data, + options={f"{CONF_STEP_PREFIX}{desc['key']}": custom_step}, + ) + entry.runtime_data = SimpleNamespace(handle=config_entry.runtime_data.handle) + + num = PIDParameterNumber(hass, entry, desc) + assert num._attr_native_step == custom_step + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("desc", PID_NUMBER_ENTITIES) +async def test_pid_number_step_defaults(hass, config_entry, desc): + """Test that PIDParameterNumber falls back to DEFAULT_STEPS when no option is set.""" + num = PIDParameterNumber(hass, config_entry, desc) + assert num._attr_native_step == DEFAULT_STEPS[desc["key"]] + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("desc", CONTROL_NUMBER_ENTITIES) +async def test_control_number_step_from_options(hass, config_entry, desc): + """Test that ControlParameterNumber reads step from CONF_STEP_PREFIX+key in options.""" + from types import SimpleNamespace + from pytest_homeassistant_custom_component.common import MockConfigEntry + + custom_step = 2.0 + entry = MockConfigEntry( + domain=DOMAIN, + entry_id=f"step_test_{desc['key']}", + data=config_entry.data, + options={f"{CONF_STEP_PREFIX}{desc['key']}": custom_step}, + ) + entry.runtime_data = SimpleNamespace(handle=config_entry.runtime_data.handle) + + num = ControlParameterNumber(hass, entry, desc) + assert num._attr_native_step == custom_step + + +@pytest.mark.usefixtures("setup_integration") +@pytest.mark.parametrize("desc", CONTROL_NUMBER_ENTITIES) +async def test_control_number_step_defaults(hass, config_entry, desc): + """Test that ControlParameterNumber falls back to DEFAULT_STEPS when no option is set.""" + num = ControlParameterNumber(hass, config_entry, desc) + assert num._attr_native_step == DEFAULT_STEPS[desc["key"]] diff --git a/tests/test_select.py b/tests/test_select.py index 251d940..a6438d6 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -22,7 +22,7 @@ async def test_pid_start_modes(hass, config_entry): for start_mode in ["Zero start", "Startup value", "Last known value"]: # reset de PID state per iteratie handle = config_entry.runtime_data.handle - handle.init_phase = True + handle.pid.set_auto_mode(False) handle.last_known_output = 80.0 handle.get_input_sensor_value = lambda: base_input