diff --git a/.devcontainer.json b/.devcontainer.json index 9fda616..5cec644 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "ludeeus/integration_blueprint", - "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye", + "image": "mcr.microsoft.com/devcontainers/python:1-3.12", "postCreateCommand": "scripts/setup", "forwardPorts": [ 8123 @@ -17,17 +17,21 @@ "ms-python.python", "github.vscode-pull-request-github", "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance" + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "ms-python.pylint" ], "settings": { "files.eol": "\n", "editor.tabSize": 4, "python.pythonPath": "/usr/bin/python3", "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "black-formatter.path": [ + "/usr/local/py-utils/bin/black" + ], "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..7a53f74 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +ARG BUILD_FROM BUILD_FROM_TAG +FROM python:3.11-slim + +ENV DEVCONTAINER=true + +COPY ./container /container +COPY ./install /install + +ARG OS_VARIANT CONTAINER_TYPE +RUN \ + bash /install/init.sh \ + && bash /install/container.sh \ + && bash /install/integration.sh \ + && bash /install/cleanup.sh + +CMD ["bash"] diff --git a/.devcontainer/container/container b/.devcontainer/container/container new file mode 100644 index 0000000..e9c6237 --- /dev/null +++ b/.devcontainer/container/container @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +make --file /opt/container/container.mk "${*:-"help"}" diff --git a/.devcontainer/container/container.mk b/.devcontainer/container/container.mk new file mode 100644 index 0000000..78110c6 --- /dev/null +++ b/.devcontainer/container/container.mk @@ -0,0 +1,12 @@ +MAKEFLAGS += --no-print-directory +SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +.DEFAULT_GOAL := help + +include /opt/container/makefiles/*.mk + +help: ## Show help + @printf " \033[1m%s\033[0m\n %s\033[32m\033[0m\n %s\033[32m\033[0m \n\n" "container" "Custom CLI used in this container" "https://github.com/ludeeus/container"; + @printf " \033[1m%s\033[0m\n %s\033[32m\033[0m \n\n" "usage:" "container [command]"; + @printf " \033[1m%s\033[0m\n" "where [command] is one of:"; + @awk 'BEGIN {FS = ":.*##";} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m container %-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST); + @echo diff --git a/.devcontainer/container/helpers/common/homeassistant/check-config.sh b/.devcontainer/container/helpers/common/homeassistant/check-config.sh new file mode 100644 index 0000000..de91bf3 --- /dev/null +++ b/.devcontainer/container/helpers/common/homeassistant/check-config.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +hass -c /config --script check_config \ No newline at end of file diff --git a/.devcontainer/container/helpers/common/homeassistant/set-version.sh b/.devcontainer/container/helpers/common/homeassistant/set-version.sh new file mode 100644 index 0000000..ade4e9e --- /dev/null +++ b/.devcontainer/container/helpers/common/homeassistant/set-version.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +read -p 'Set Home Assistant version: ' -r version +python3 -m pip --disable-pip-version-check install --upgrade homeassistant=="$version" + +if [[ -n "$POST_SET_VERSION_HOOK" ]]; then + "$POST_SET_VERSION_HOOK" "$version" +fi \ No newline at end of file diff --git a/.devcontainer/container/helpers/common/homeassistant/start.sh b/.devcontainer/container/helpers/common/homeassistant/start.sh new file mode 100644 index 0000000..080e956 --- /dev/null +++ b/.devcontainer/container/helpers/common/homeassistant/start.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# shellcheck source=/dev/null + +source /opt/container/helpers/common/paths.sh +mkdir -p /config + +if test -f "$(workspacePath)config/configuration.yaml"; then + echo "Copy configuration.yaml" + ln -sf "$(workspacePath)config/configuration.yaml" /config/configuration.yaml || echo "config/configuration.yaml are missing" +fi + +if test -f "$(workspacePath)config/ui-lovelace.yaml"; then + echo "Copy ui-lovelace.yaml" + ln -sf "$(workspacePath)config/ui-lovelace.yaml" /config/ui-lovelace.yaml || echo "" +fi + +if test -f "$(workspacePath)config/secrets.yaml"; then + echo "Copy secrets.yaml" + ln -sf "$(workspacePath)config/secrets.yaml" /config/secrets.yaml || echo "" +fi + +if test -d "$(workspacePath)custom_components"; then + echo "Symlink the custom component directory" + + if test -d "$(workspacePath)custom_components"; then + rm -R /config/custom_components + fi + + ln -sf "$(workspacePath)custom_components/" /config/custom_components || echo "Could not copy the custom_component" exit 1 +elif test -f "__init__.py"; then + echo "Having the component in the root is currently not supported" +fi + +echo "Start Home Assistant" +if ! [ -x "$(command -v hass)" ]; then + echo "Home Assistant is not installed, running installation." + python3 -m pip --disable-pip-version-check install --upgrade git+https://github.com/home-assistant/home-assistant.git@dev +fi +hass --script ensure_config -c /config +hass -c /config diff --git a/.devcontainer/container/helpers/common/paths.sh b/.devcontainer/container/helpers/common/paths.sh new file mode 100644 index 0000000..d2ed67d --- /dev/null +++ b/.devcontainer/container/helpers/common/paths.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +function workspacePath { + if [[ -n "$WORKSPACE_DIRECTORY" ]]; then + echo "${WORKSPACE_DIRECTORY}/" + else + echo "$(find /workspaces -mindepth 1 -maxdepth 1 -type d | tail -1)/" + fi +} \ No newline at end of file diff --git a/.devcontainer/container/helpers/integration/init.sh b/.devcontainer/container/helpers/integration/init.sh new file mode 100644 index 0000000..7118acb --- /dev/null +++ b/.devcontainer/container/helpers/integration/init.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# shellcheck source=/dev/null + +source /opt/container/helpers/common/paths.sh + + +if test -d "$(workspacePath).git"; then + echo ".git exsist in $(workspacePath), existing initializing" + exit 1 +fi + +echo "Initializing dev env for integration" +rm -R /tmp/init > /dev/null 2>&1 + +git clone https://github.com/custom-components/integration-blueprint.git /tmp/init + +rm -R /tmp/init/.git +rm -R /tmp/init/.devcontainer +cp -a /tmp/init/. "$(workspacePath)" +cd "$(workspacePath)" || exit 1 +git init diff --git a/.devcontainer/container/makefiles/integration.mk b/.devcontainer/container/makefiles/integration.mk new file mode 100644 index 0000000..87fa68b --- /dev/null +++ b/.devcontainer/container/makefiles/integration.mk @@ -0,0 +1,20 @@ +start: ## Start Home Assistant with the integration loaded + @bash /opt/container/helpers/common/homeassistant/start.sh + +set-version: ## Set Home Assistant version + @bash /opt/container/helpers/common/homeassistant/set-version.sh + +install: ## Install Home Assistant dev in the container + @python3 -m pip --disable-pip-version-check install --upgrade git+https://github.com/home-assistant/home-assistant.git@dev + +upgrade: ## Upgrade Home Assistant to latest dev in the container + install + +run: + start + +check-config: ## Check Home Assistant config + @hass -c /config --script check_config + +init: ## Initialize the dev env + @bash /opt/container/helpers/integration/init.sh \ No newline at end of file diff --git a/.devcontainer/install/cleanup.sh b/.devcontainer/install/cleanup.sh new file mode 100644 index 0000000..f196398 --- /dev/null +++ b/.devcontainer/install/cleanup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +echo -e "\\033[0;34mRunning cleanup script 'cleanup.sh'\\033[0m" + +apt-get clean -y +rm -fr /var/lib/apt/lists/* +rm -fr /tmp/* /var/{cache,log}/* + +rm -fr /container +rm -fr /install diff --git a/.devcontainer/install/container.sh b/.devcontainer/install/container.sh new file mode 100644 index 0000000..82122a7 --- /dev/null +++ b/.devcontainer/install/container.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e +echo -e "\\033[0;34mRunning install script 'container.sh'\\033[0m" + +export DEBIAN_FRONTEND=noninteractive + +apt-get update +apt-get install -y --no-install-recommends \ + make + +mkdir -p /opt/container/makefiles +mkdir -p /opt/container/helpers +touch /opt/container/makefiles/dummy.mk + +cp /container/container.mk /opt/container/container.mk +cp -r /container/helpers/common /opt/container/helpers/common + +cp /container/container /usr/bin/container +chmod +x /usr/bin/container + +cp /container/makefiles/integration.mk /opt/container/makefiles/integration.mk +cp -r /container/helpers/integration /opt/container/helpers/integration + +container help diff --git a/.devcontainer/install/init.sh b/.devcontainer/install/init.sh new file mode 100644 index 0000000..868efb4 --- /dev/null +++ b/.devcontainer/install/init.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +uname -m +printenv diff --git a/.devcontainer/install/integration.sh b/.devcontainer/install/integration.sh new file mode 100644 index 0000000..bb3fbb5 --- /dev/null +++ b/.devcontainer/install/integration.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -e +echo -e "\\033[0;34mRunning install script 'integration.sh'\\033[0m" + +export DEBIAN_FRONTEND=noninteractive + +apt-get update +apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + ffmpeg \ + gcc \ + git \ + jq \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libbz2-dev \ + libcap-dev \ + libffi-dev \ + libjpeg-dev \ + liblzma-dev \ + libncurses5-dev \ + libncursesw5-dev \ + libpcap-dev \ + libreadline-dev \ + libsqlite3-dev \ + libssl-dev \ + libswresample-dev \ + libswscale-dev \ + llvm \ + shellcheck \ + tar \ + tk-dev \ + wget \ + xz-utils \ + zlib1g-dev + +python3 -m pip --disable-pip-version-check install --upgrade \ + git+https://github.com/home-assistant/home-assistant.git@dev +python3 -m pip --disable-pip-version-check install --upgrade wheel setuptools + +# Fix issue https://github.com/home-assistant/core/issues/95192 +python3 -m pip --disable-pip-version-check install --upgrade git+https://github.com/boto/botocore urllib3~=1.26 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9c65fef..72ae28f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,7 +1,7 @@ --- name: "Bug report" description: "Report a bug with the integration" -labels: "Bug" +labels: "bug" body: - type: markdown attributes: @@ -9,7 +9,7 @@ body: - type: textarea attributes: label: "System Health details" - description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io//more-info/system-health#github-issues)" + description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)" validations: required: true - type: checkboxes @@ -22,7 +22,7 @@ body: required: true - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). required: true - - label: This issue is not a duplicate issue of currently [previous issues](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Bug%22+).. + - label: This issue is not a duplicate issue of any [previous issues](https://github.com/Limych/ha-average/issues?q=is%3Aissue+label%3A%22bug%22+).. required: true - type: textarea attributes: @@ -33,7 +33,7 @@ body: - type: textarea attributes: label: Reproduction steps - description: "Without steps to reproduce, it will be hard to fix, it is very important that you fill out this part, issues without it will be closed" + description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed." value: | 1. 2. @@ -44,7 +44,7 @@ body: - type: textarea attributes: label: "Debug logs" - description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue." + description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to install _everything_ from startup of Home Assistant to the point where you encounter the issue." render: text validations: required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 5df85e3..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve component -title: '' -labels: 'bug' -assignees: '' - ---- - - - -**Environment** - - -- Home Assistant Core release with the issue: -- This custom component release with the issue: -- Last working this custom component release (if known): -- Operating environment (Home Assistant/Supervisor/Docker/venv): - -**Describe the bug** - - - -**Configuration.yaml** -```yaml - -Add your configs here if any. - -``` - -**Steps to Reproduce** - - - -**Expected behavior** - - - -**Debug log** - -```text - -Add your logs here. - -``` - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c2d2583..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: 'enhancement' -assignees: '' - ---- - - - -**Is your feature request related to a problem? Please describe.** - - - -**Describe the solution you'd like** - - - -**Describe alternatives you've considered** - - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 433467b..fb1fb04 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,7 +1,7 @@ --- name: "Feature request" description: "Suggest an idea for this project" -labels: "Feature+Request" +labels: "enhancement" body: - type: markdown attributes: @@ -14,7 +14,7 @@ body: required: true - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). required: true - - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). + - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/Limych/ha-average/issues?q=is%3Aissue+label%3A%22enhancement%22+). required: true - type: textarea diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 03c8954..60e04b6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/issue-lock.yml b/.github/workflows/issue-lock.yml index 5729d67..c03ded0 100644 --- a/.github/workflows/issue-lock.yml +++ b/.github/workflows/issue-lock.yml @@ -16,7 +16,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-lock-inactive-days: 30 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 88fc142..4e07fbc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,12 +14,12 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v4" + uses: "actions/checkout@v4.1.0" - name: "Set up Python" - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version-file: 'pyproject.toml' cache: "pip" - name: "Install requirements" diff --git a/.github/workflows/py-dead-code.yml b/.github/workflows/py-dead-code.yml index db0609e..5e3744b 100644 --- a/.github/workflows/py-dead-code.yml +++ b/.github/workflows/py-dead-code.yml @@ -12,15 +12,15 @@ jobs: uses: actions/checkout@v4 - run: | - echo "package=$(ls -F | grep \/$ | grep -v "bin\|examples\|tests" | sed -n "s/\///g;1p")" >> $GITHUB_ENV + echo "package=$(ls -F | grep \/$ | grep -v "scripts\|examples\|tests\|config" | sed -n "s/\///g;1p")" >> $GITHUB_ENV - name: "Set up Python" - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version-file: 'pyproject.toml' - name: "Cache pip" - uses: actions/cache@v3 + uses: actions/cache@v4 with: # This path is specific to Ubuntu path: ~/.cache/pip diff --git a/.github/workflows/py-test.yml b/.github/workflows/py-test.yml index f6c3288..77d6e6d 100644 --- a/.github/workflows/py-test.yml +++ b/.github/workflows/py-test.yml @@ -23,12 +23,12 @@ jobs: echo "package=$(ls -F | grep \/$ | grep -v "bin\|examples\|tests" | sed -n "s/\///g;1p")" >> $GITHUB_ENV - name: "Set up Python" - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version-file: 'pyproject.toml' - name: "Cache pip" - uses: actions/cache@v3 + uses: actions/cache@v4 with: # This path is specific to Ubuntu path: ~/.cache/pip @@ -63,21 +63,17 @@ jobs: name: "Test package" needs: lint runs-on: ubuntu-latest - strategy: - max-parallel: 3 - matrix: - python-version: ['3.9', '3.10', '3.11'] steps: - name: "Checkout code" uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - name: "Set up Python" + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version-file: 'pyproject.toml' - name: "Cache pip" - uses: actions/cache@v3 + uses: actions/cache@v4 with: # This path is specific to Ubuntu path: ~/.cache/pip @@ -104,25 +100,16 @@ jobs: echo '"""Stub."""' >custom_components/__init__.py fi - - name: "Run tests with pytest" - if: matrix.python-version != '3.11' - run: | - pytest --basetemp=$RUNNER_TEMP --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar - ./scripts/check_dirty - - name: "Install Coveralls" - if: matrix.python-version == '3.11' run: | pip install pytest-xdist coveralls - name: "Run tests with pytest & Calculate coverage" - if: matrix.python-version == '3.11' run: | pytest --basetemp=$RUNNER_TEMP --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= ./scripts/check_dirty - name: "Send coverage to Coveralls" - if: matrix.python-version == '3.11' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebdada0..0fd446b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,8 +11,8 @@ jobs: name: "Publish new release" runs-on: ubuntu-latest steps: - - name: "Check out repository" - uses: actions/checkout@v4 + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.0" - working-directory: ./custom_components run: | @@ -36,7 +36,7 @@ jobs: - name: "Release" if: env.release_version != '' && success() - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: ${{ env.basedir }}/${{ env.package }}.zip @@ -54,13 +54,13 @@ jobs: - name: "Set up Python" if: env.release_version != '' && success() - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version-file: 'pyproject.toml' - name: "Cache pip" if: env.release_version != '' && success() - uses: actions/cache@v3 + uses: actions/cache@v4 with: # This path is specific to Ubuntu path: ~/.cache/pip diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1ef40ce..3f643d1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v4" + uses: "actions/checkout@v4.1.0" - name: "Run hassfest validation" uses: "home-assistant/actions/hassfest@master" @@ -27,7 +27,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v4" + uses: "actions/checkout@v4.1.0" - name: "Run HACS validation" uses: "hacs/action@main" diff --git a/.gitignore b/.gitignore index b1d2583..85f2aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ coverage.xml # Home Assistant configuration config/* +!config/bootstrap.sh !config/configuration.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aee2e89..d602d3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,20 +7,20 @@ repos: language: script files: ^(custom_components/.+/const\.py|requirements\.txt)$ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.241 + rev: v0.3.5 hooks: - id: ruff args: - --fix files: ^(custom_components|bin|tests)/.+\.py$ - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.2 hooks: - id: pyupgrade - args: [ --py310-plus ] + args: [ --py312-plus ] stages: [manual] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 24.3.0 hooks: - id: black args: @@ -37,7 +37,7 @@ repos: files: ^(custom_components|bin|tests)/.+\.py$ stages: [manual] - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.8 hooks: - id: bandit args: diff --git a/.ruff.toml b/.ruff.toml index 7a8331a..eb68b8c 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,8 +1,8 @@ # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml -target-version = "py310" +target-version = "py312" -select = [ +lint.select = [ "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "C", # complexity @@ -26,7 +26,7 @@ select = [ "W", # pycodestyle ] -ignore = [ +lint.ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line @@ -38,11 +38,11 @@ ignore = [ "E731", # do not assign a lambda expression, use a def ] -[flake8-pytest-style] +[lint.flake8-pytest-style] fixture-parentheses = false -[pyupgrade] +[lint.pyupgrade] keep-runtime-typing = true -[mccabe] -max-complexity = 25 \ No newline at end of file +[lint.mccabe] +max-complexity = 25 diff --git a/README.md b/README.md index e551fac..36dc1cf 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ I also suggest you [visit the support topic][forum] on the community forum. ### Install from HACS (recommended) 1. Have [HACS][hacs] installed, this will allow you to easily manage and track updates. -1. Search for "Average". +1. Search in HACS for "Average" integration or just press the button below:\ +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)][hacs-repository] 1. Click Install below the found integration. ... then if you want to use `configuration.yaml` to configure sensor... @@ -343,6 +344,7 @@ See separate [license file](LICENSE.md) for full text. [commits]: https://github.com/Limych/ha-average/commits/dev [hacs-shield]: https://img.shields.io/badge/HACS-Default-orange.svg?style=popout [hacs]: https://hacs.xyz +[hacs-repository]: https://my.home-assistant.io/redirect/hacs_repository/?owner=Limych&repository=ha-average&category=integration [exampleimg]: https://github.com/Limych/ha-average/raw/dev/example.png [forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=popout [forum]: https://community.home-assistant.io/t/average-sensor/111674 diff --git a/config/bootstrap.sh b/config/bootstrap.sh new file mode 100755 index 0000000..4b68a67 --- /dev/null +++ b/config/bootstrap.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +ROOT="$( cd "$( dirname "$(readlink -f "$0")" )/.." >/dev/null 2>&1 && pwd )" + +GITHUB_TOKEN=$(grep github_token ${ROOT}/secrets.yaml | cut -d' ' -f2) +FILES=$(grep "{GITHUB_TOKEN}" ${ROOT}/requirements.txt | sed "s/{GITHUB_TOKEN}/${GITHUB_TOKEN}/g" | tr "\r\n" " ") + +[ -z "${FILES}" ] || python3 -m pip install --upgrade ${FILES} diff --git a/custom_components/average/__init__.py b/custom_components/average/__init__.py index 966dba5..541089f 100644 --- a/custom_components/average/__init__.py +++ b/custom_components/average/__init__.py @@ -2,8 +2,7 @@ # Creative Commons BY-NC-SA 4.0 International Public License # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -""" -The Average Sensor. +"""The Average Sensor. For more details about this sensor, please refer to the documentation at https://github.com/Limych/ha-average/ @@ -13,9 +12,9 @@ import logging import voluptuous as vol + from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType @@ -29,16 +28,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Print startup message _LOGGER.info(STARTUP_MESSAGE) - # await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - async def reload_service_handler(service: ServiceCall) -> None: """Reload all average sensors from config.""" - print("+++++++++++++++++++++++++") - print(component) - # print(hass.data[DATA_INSTANCES]["sensor"].entities[0]) - await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) hass.services.async_register( diff --git a/custom_components/average/const.py b/custom_components/average/const.py index abcfec1..ff2e171 100644 --- a/custom_components/average/const.py +++ b/custom_components/average/const.py @@ -3,6 +3,7 @@ For more details about this sensor, please refer to the documentation at https://github.com/Limych/ha-average/ """ + from datetime import timedelta from typing import Final @@ -11,7 +12,7 @@ NAME: Final = "Average Sensor" DOMAIN: Final = "average" -VERSION: Final = "2.3.1" +VERSION: Final = "2.3.5-alpha" ISSUE_URL: Final = "https://github.com/Limych/ha-average/issues" STARTUP_MESSAGE: Final = f""" @@ -24,7 +25,7 @@ ------------------------------------------------------------------- """ -PLATFORMS = [ +PLATFORMS: Final = [ Platform.SENSOR, ] diff --git a/custom_components/average/manifest.json b/custom_components/average/manifest.json index 6e71a64..982f7ee 100644 --- a/custom_components/average/manifest.json +++ b/custom_components/average/manifest.json @@ -14,8 +14,6 @@ "documentation": "https://github.com/Limych/ha-average", "iot_class": "calculated", "issue_tracker": "https://github.com/Limych/ha-average/issues", - "requirements": [ - "colorlog==6.7.0" - ], - "version": "2.3.1" + "requirements": [], + "version": "2.3.5-alpha" } \ No newline at end of file diff --git a/custom_components/average/sensor.py b/custom_components/average/sensor.py index be270e7..da70109 100644 --- a/custom_components/average/sensor.py +++ b/custom_components/average/sensor.py @@ -14,7 +14,7 @@ import logging import math import numbers -from typing import Any, Optional +from typing import Any from _sha1 import sha1 import voluptuous as vol @@ -22,7 +22,11 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.group import expand_entity_ids from homeassistant.components.recorder import get_instance, history -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import ( @@ -32,16 +36,22 @@ CONF_ENTITIES, CONF_NAME, CONF_UNIQUE_ID, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, + split_entity_id, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util import Throttle import homeassistant.util.dt as dt_util from homeassistant.util.unit_conversion import TemperatureConverter @@ -151,7 +161,7 @@ class AverageSensor(SensorEntity): def __init__( self, hass: HomeAssistant, - unique_id: Optional[str], + unique_id: str | None, name: str, start, end, @@ -181,7 +191,7 @@ def __init__( self._attr_native_value = None self._attr_native_unit_of_measurement = None self._attr_icon = None - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_device_class = None # self._attr_unique_id = ( @@ -216,7 +226,7 @@ def available(self) -> bool: return self.available_sources > 0 and self._has_state(self._attr_native_value) @property - def extra_state_attributes(self) -> Optional[Mapping[str, Any]]: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" state_attr = { attr: getattr(self, attr) @@ -230,7 +240,9 @@ async def async_added_to_hass(self) -> None: # pylint: disable=unused-argument @callback - async def async_sensor_state_listener(entity, old_state, new_state): + async def async_sensor_state_listener( + event: Event[EventStateChangedData], + ) -> None: """Handle device state changes.""" last_state = self._attr_native_value await self._async_update_state() @@ -244,10 +256,10 @@ async def async_sensor_startup(event): if self._has_period: self.async_schedule_update_ha_state(True) else: - async_track_state_change( + async_track_state_change_event( self.hass, self.sources, async_sensor_state_listener ) - await async_sensor_state_listener(None, None, None) + await async_sensor_state_listener(Event("startup")) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_sensor_startup) @@ -261,7 +273,7 @@ def _has_state(state) -> bool: "", ] - def _get_temperature(self, state: State) -> Optional[float]: + def _get_temperature(self, state: State) -> float | None: """Get temperature value from entity.""" ha_unit = self.hass.config.units.temperature_unit domain = split_entity_id(state.entity_id)[0] @@ -288,7 +300,7 @@ def _get_temperature(self, state: State) -> Optional[float]: return temperature - def _get_state_value(self, state: State) -> Optional[float]: + def _get_state_value(self, state: State) -> float | None: """Return value of given entity state and count some sensor attributes.""" state = self._get_temperature(state) if self._temperature_mode else state.state if not self._has_state(state): @@ -396,9 +408,7 @@ async def _async_update_period(self): # pylint: disable=too-many-branches if start > now: # History hasn't been written yet for this period return - if now < end: - # No point in making stats of the future - end = now + end = min(end, now) # No point in making stats of the future self._period = start, end self.start = start.replace(microsecond=0).isoformat() @@ -415,13 +425,13 @@ def _init_mode(self, state: State): ATTR_UNIT_OF_MEASUREMENT ) self._temperature_mode = ( - self._attr_device_class == DEVICE_CLASS_TEMPERATURE + self._attr_device_class == SensorDeviceClass.TEMPERATURE or domain in (WEATHER_DOMAIN, CLIMATE_DOMAIN, WATER_HEATER_DOMAIN) or self._attr_native_unit_of_measurement in TEMPERATURE_UNITS ) if self._temperature_mode: _LOGGER.debug("%s is a temperature entity.", state.entity_id) - self._attr_device_class = DEVICE_CLASS_TEMPERATURE + self._attr_device_class = SensorDeviceClass.TEMPERATURE self._attr_native_unit_of_measurement = ( self.hass.config.units.temperature_unit ) @@ -509,7 +519,7 @@ async def _async_update_state( ) if ( - entity_id not in history_list.keys() + entity_id not in history_list or history_list[entity_id] is None or len(history_list[entity_id]) == 0 ): @@ -544,14 +554,16 @@ async def _async_update_state( last_time = current_time # Count time elapsed between last history state and now - if last_state is not None: + if last_state is None: + value = None + else: last_elapsed = end_ts - last_time value += last_state * last_elapsed elapsed += last_elapsed + if elapsed: + value /= elapsed trending_last_state = last_state - if elapsed: - value /= elapsed _LOGGER.debug("Historical average state: %s", value) if isinstance(value, numbers.Number): diff --git a/hacs.json b/hacs.json index 290a533..c54de86 100644 --- a/hacs.json +++ b/hacs.json @@ -1,8 +1,8 @@ { "name": "Average Sensor", - "filename": "average_sensor.zip", + "filename": "average.zip", "hide_default_branch": true, - "homeassistant": "2023.1.0", + "homeassistant": "2024.4.0", "render_readme": true, "zip_release": true } diff --git a/pylintrc b/pylintrc index 850e50d..7b6263c 100644 --- a/pylintrc +++ b/pylintrc @@ -3,7 +3,7 @@ ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 -load-plugins=pylint_strict_informational +fail-on=I persistent=no extension-pkg-whitelist=ciso8601 diff --git a/pyproject.toml b/pyproject.toml index cb14c1b..1b2b658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,8 @@ +[project] +requires-python = ">=3.12" + [tool.black] -target-version = ["py310"] +target-version = ["py312"] extend-exclude = "/generated/" [tool.isort] @@ -17,7 +20,7 @@ forced_separate = [ combine_as_imports = true [tool.pylint.MAIN] -py-version = "3.10" +py-version = "3.12" ignore = [ "tests", ] @@ -162,7 +165,7 @@ log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" [tool.ruff] -target-version = "py310" +target-version = "py312" select = [ "C", # complexity diff --git a/requirements-dev.txt b/requirements-dev.txt index ecfe215..3357394 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ -r requirements-test.txt -black~=23.9.1 -packaging~=23.0 -pre-commit~=3.4 -PyGithub~=1.59 -pyupgrade~=3.3 -yamllint~=1.32 +black~=24.4 +packaging~=24.0 +pre-commit~=3.7 +PyGithub~=2.3 +pyupgrade~=3.15 +yamllint~=1.35 diff --git a/requirements-test.txt b/requirements-test.txt index 3480b95..2fd885e 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,12 +1,15 @@ -r requirements.txt -flake8~=6.1 +colorlog~=6.8 +flake8~=7.0 flake8-docstrings~=1.7 -fnv-hash-fast~=0.4 -mypy~=1.5.1 +fnv-hash-fast~=0.5 +mypy~=1.10 psutil-home-assistant==0.0.1 -pylint~=2.15 +pylint~=3.1 pylint-strict-informational==0.1 pytest>=7.2 pytest-cov>=3.0 -pytest-homeassistant-custom-component>=0.12 +pytest-homeassistant-custom-component>=0.13 pytest-asyncio>=0.20 +tzdata +ruff>=0.4 diff --git a/requirements.txt b/requirements.txt index 82dbb56..02a0900 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -colorlog==6.7.0 -homeassistant>=2023.1.0 -pip>=21.0,<23.2 +homeassistant>=2024.4.0 +pip>=24.0 diff --git a/scripts/devcontainer b/scripts/devcontainer index c7f7791..b586313 100755 --- a/scripts/devcontainer +++ b/scripts/devcontainer @@ -16,7 +16,7 @@ workdir="/workspaces/${workspace}" container="dev-${workspace}" port="127.0.0.1:9123:8123" -image="ghcr.io/ludeeus/devcontainer/integration:stable" +image="devcontainer" volume="${ROOT}:${workdir}" cmd="menu" @@ -72,14 +72,15 @@ docker_start() { } bootstrap() { - if test -f "${ROOT}/.devcontainer/bootstrap.sh"; then + if test -f "${ROOT}/config/bootstrap.sh"; then log.info "Execute bootstrap.sh..." - ${docker} exec -it -w "${workdir}" "${container}" .devcontainer/bootstrap.sh "$1" + ${docker} exec -it -w "${workdir}" "${container}" config/bootstrap.sh "$1" fi } if ! ${docker} ps -a | grep -wq ${container} && [[ "${cmd}" != "down" ]]; then log.info "Create container..." + ${docker} build -t "${image}" "${ROOT}/.devcontainer/" ${docker} create -it --name "${container}" -p "${port}" -v "${volume}" "${image}" docker_start diff --git a/scripts/setup b/scripts/setup index 650a348..170bb07 100755 --- a/scripts/setup +++ b/scripts/setup @@ -19,6 +19,8 @@ if [ ! -d "venv" ]; then ${python} -m venv ./venv source ./venv/bin/activate python="${ROOT}/venv/bin/python3" +else + ${python} -m venv ./venv fi pip="${python} -m pip" diff --git a/scripts/update b/scripts/update index c012e41..312ec4e 100755 --- a/scripts/update +++ b/scripts/update @@ -9,8 +9,12 @@ cd "${ROOT}" if git branch -r | grep -q "blueprint/dev" ; then git fetch blueprint dev +elif git branch -r | grep -q "blueprint/develop" ; then + git fetch blueprint develop elif git branch -r | grep -q "blueprint/master" ; then git fetch blueprint master +elif git branch -r | grep -q "blueprint/main" ; then + git fetch blueprint main fi git fetch diff --git a/scripts/update_manifest b/scripts/update_manifest index 52ffeca..b603647 100755 --- a/scripts/update_manifest +++ b/scripts/update_manifest @@ -18,11 +18,11 @@ component=$(ls -q "${ROOT}/custom_components/" | grep -v __ | head -n 1) const_path="custom_components/${component}/const.py" reqs_path="requirements.txt" -name=$(grep "^NAME: Final =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") -domain=$(grep "^DOMAIN: Final =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") -version=$(grep "^VERSION: Final =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") -issue_url=$(grep "^ISSUE_URL: Final =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") -ha_version=$(grep "^homeassistant>=" ${reqs_path} | sed -E "s/^[^=]+=(\S*).*$/\\1/") +name=$(grep -Ei "^NAME(: Final)? =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") +domain=$(grep -Ei "^DOMAIN(: Final)? =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") +version=$(grep -Ei "^VERSION(: Final)? =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") +issue_url=$(grep -Ei "^ISSUE_URL(: Final)? =" ${const_path} | sed -E "s/^[^\"]+\"([^\"]*).*$/\\1/") +ha_version=$(grep -Ei "^homeassistant>=" ${reqs_path} | sed -E "s/^[^=]+=(\S*).*$/\\1/") log.info "Update manifest.json data..." manifest_path="custom_components/${component}/manifest.json" diff --git a/tests/bandit.yaml b/tests/bandit.yaml index ebd284e..dcacabd 100644 --- a/tests/bandit.yaml +++ b/tests/bandit.yaml @@ -12,6 +12,5 @@ tests: - B318 - B319 - B320 - - B325 - B602 - B604 diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 0ef4ebe..f8aa9f4 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -19,7 +19,7 @@ check_period_keys, ) from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.sensor import DOMAIN as SENSOR, SensorDeviceClass from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import ( @@ -29,10 +29,9 @@ CONF_ENTITIES, CONF_NAME, CONF_PLATFORM, - DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers.template import Template @@ -244,7 +243,7 @@ async def test__get_temperature(default_sensor): state = State( "sensor.test", "125", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, dt_util.now(), ) assert round(default_sensor._get_temperature(state), 3) == 51.667 @@ -252,7 +251,7 @@ async def test__get_temperature(default_sensor): state = State( "sensor.test", "", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, dt_util.now(), ) assert default_sensor._get_temperature(state) is None @@ -260,7 +259,7 @@ async def test__get_temperature(default_sensor): state = State( "sensor.test", "qwe", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, dt_util.now(), ) assert default_sensor._get_temperature(state) is None @@ -321,7 +320,7 @@ async def test__init_mode(hass: HomeAssistant, default_sensor, caplog): "sensor.test", "None", { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, }, ) @@ -333,7 +332,7 @@ async def test__init_mode(hass: HomeAssistant, default_sensor, caplog): default_sensor._init_mode(state) assert default_sensor._temperature_mode is True - assert default_sensor._attr_device_class is DEVICE_CLASS_TEMPERATURE + assert default_sensor._attr_device_class is SensorDeviceClass.TEMPERATURE assert ( default_sensor._attr_native_unit_of_measurement is hass.config.units.temperature_unit @@ -358,7 +357,7 @@ async def test__init_mode(hass: HomeAssistant, default_sensor, caplog): default_sensor._init_mode(state) assert default_sensor._temperature_mode is True - assert default_sensor._attr_device_class is DEVICE_CLASS_TEMPERATURE + assert default_sensor._attr_device_class is SensorDeviceClass.TEMPERATURE assert ( default_sensor._attr_native_unit_of_measurement == hass.config.units.temperature_unit @@ -380,7 +379,7 @@ async def test__init_mode(hass: HomeAssistant, default_sensor, caplog): default_sensor._init_mode(state) assert default_sensor._temperature_mode is True - assert default_sensor._attr_device_class is DEVICE_CLASS_TEMPERATURE + assert default_sensor._attr_device_class is SensorDeviceClass.TEMPERATURE assert ( default_sensor._attr_native_unit_of_measurement == hass.config.units.temperature_unit