Compare commits

..

3 Commits

Author SHA1 Message Date
Robert Resch
b5588d9cc4 Merge branch 'dev' into edenhaus-prek 2026-01-12 20:13:10 +01:00
Robert Resch
052bb4b657 Fix 2026-01-12 20:09:05 +01:00
Robert Resch
6844253260 Replace pre-commit by prek 2026-01-07 14:51:54 +01:00
14 changed files with 38 additions and 463 deletions

View File

@@ -847,8 +847,8 @@ rules:
## Development Commands
### Code Quality & Linting
- **Run all linters on all files**: `pre-commit run --all-files`
- **Run linters on staged files only**: `pre-commit run`
- **Run all linters on all files**: `prek run --all-files`
- **Run linters on staged files only**: `prek run`
- **PyLint on everything** (slow): `pylint homeassistant`
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
- **MyPy type checking (whole project)**: `mypy homeassistant/`

View File

@@ -59,7 +59,6 @@ env:
# 15 is the latest version
# - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
UV_CACHE_DIR: /tmp/uv-cache
APT_CACHE_BASE: /home/runner/work/apt
APT_CACHE_DIR: /home/runner/work/apt/cache
@@ -83,7 +82,6 @@ jobs:
integrations_glob: ${{ steps.info.outputs.integrations_glob }}
integrations: ${{ steps.integrations.outputs.changes }}
apt_cache_key: ${{ steps.generate_apt_cache_key.outputs.key }}
pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }}
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
requirements: ${{ steps.core.outputs.requirements }}
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
@@ -111,11 +109,6 @@ jobs:
hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}-${{
hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT
- name: Generate partial pre-commit restore key
id: generate_pre-commit_cache_key
run: >-
echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{
hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Generate partial apt restore key
id: generate_apt_cache_key
run: |
@@ -244,8 +237,8 @@ jobs:
echo "skip_coverage: ${skip_coverage}"
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
pre-commit:
name: Prepare pre-commit base
prek:
name: Run prek checks
runs-on: *runs-on-ubuntu
needs: [info]
if: |
@@ -254,147 +247,23 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- *checkout
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-pre-commit-venv >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: *actions-cache
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
key: &key-pre-commit-env >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
. venv/bin/activate
pre-commit install-hooks
lint-ruff-format:
name: Check ruff-format
runs-on: *runs-on-ubuntu
needs: &needs-pre-commit
- info
- pre-commit
steps:
- *checkout
- *setup-python-default
- &cache-restore-pre-commit-venv
name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
fail-on-cache-miss: true
key: *key-pre-commit-venv
- &cache-restore-pre-commit-env
name: Restore pre-commit environment from cache
id: cache-precommit
uses: *actions-cache-restore
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: *key-pre-commit-env
- name: Run ruff-format
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff-format --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
lint-ruff:
name: Check ruff
runs-on: *runs-on-ubuntu
needs: *needs-pre-commit
steps:
- *checkout
- *setup-python-default
- *cache-restore-pre-commit-venv
- *cache-restore-pre-commit-env
- name: Run ruff
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
lint-other:
name: Check other linters
runs-on: *runs-on-ubuntu
needs: *needs-pre-commit
steps:
- *checkout
- *setup-python-default
- *cache-restore-pre-commit-venv
- *cache-restore-pre-commit-env
- name: Register yamllint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
- name: Run yamllint
run: |
. venv/bin/activate
pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure
- name: Register check-json problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-json.json"
- name: Run check-json
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-json --all-files --show-diff-on-failure
- name: Run prettier (fully)
if: needs.info.outputs.test_full_suite == 'true'
run: |
. venv/bin/activate
pre-commit run --hook-stage manual prettier --all-files --show-diff-on-failure
- name: Run prettier (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
shopt -s globstar
pre-commit run --hook-stage manual prettier --show-diff-on-failure --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
- name: Register check executables problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
- name: Run executables check
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files --show-diff-on-failure
- name: Register codespell problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run codespell
run: |
. venv/bin/activate
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
- name: Run prek
uses: j178/prek-action@91fd7d7cf70ae1dee9f4f44e7dfa5d1073fe6623 # v1.0.11
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
RUFF_OUTPUT_FORMAT: github
lint-hadolint:
name: Check ${{ matrix.file }}
@@ -434,7 +303,7 @@ jobs:
- &setup-python-matrix
name: Set up Python ${{ matrix.python-version }}
id: python
uses: *actions-setup-python
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -447,7 +316,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: *actions-cache
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-python-venv >-
@@ -562,7 +431,7 @@ jobs:
steps:
- &cache-restore-apt
name: Restore apt cache
uses: *actions-cache-restore
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
fail-on-cache-miss: true
@@ -579,7 +448,13 @@ jobs:
-o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \
libturbojpeg
- *checkout
- *setup-python-default
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: *actions-setup-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- &cache-restore-python-default
name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
@@ -782,9 +657,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
steps:
- *cache-restore-apt
@@ -823,9 +696,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
- prepare-pytest-full
if: |
@@ -949,9 +820,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'
@@ -1066,9 +935,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'
@@ -1202,9 +1069,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'

View File

@@ -46,7 +46,7 @@ repos:
# Run `python-typing-update` hook manually from time to time
# to update python typing syntax.
# Will require manual work, before submitting changes!
# pre-commit run --hook-stage manual python-typing-update --all-files
# prek run --hook-stage manual python-typing-update --all-files
- id: python-typing-update
stages: [manual]
args:

6
.vscode/tasks.json vendored
View File

@@ -45,7 +45,7 @@
{
"label": "Ruff",
"type": "shell",
"command": "pre-commit run ruff-check --all-files",
"command": "prek run ruff-check --all-files",
"group": {
"kind": "test",
"isDefault": true
@@ -57,9 +57,9 @@
"problemMatcher": []
},
{
"label": "Pre-commit",
"label": "Prek",
"type": "shell",
"command": "pre-commit run --show-diff-on-failure",
"command": "prek run --show-diff-on-failure",
"group": {
"kind": "test",
"isDefault": true

View File

@@ -33,7 +33,7 @@ from .const import (
from .coordinator import TibberDataAPICoordinator
from .services import async_setup_services
PLATFORMS = [Platform.BINARY_SENSOR, Platform.NOTIFY, Platform.SENSOR]
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

View File

@@ -1,143 +0,0 @@
"""Support for Tibber binary sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from tibber.data_api import TibberDevice
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class TibberBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Tibber binary sensor entity."""
is_on_fn: Callable[[str | None], bool | None]
def _connector_status_is_on(value: str | None) -> bool | None:
"""Map connector status value to binary sensor state."""
if value == "connected":
return True
if value == "disconnected":
return False
return None
def _charging_status_is_on(value: str | None) -> bool | None:
"""Map charging status value to binary sensor state."""
if value == "charging":
return True
if value == "idle":
return False
return None
def _device_status_is_on(value: str | None) -> bool | None:
"""Map device status value to binary sensor state."""
if value == "on":
return True
if value == "off":
return False
return None
DATA_API_BINARY_SENSORS: tuple[TibberBinarySensorEntityDescription, ...] = (
TibberBinarySensorEntityDescription(
key="connector.status",
device_class=BinarySensorDeviceClass.PLUG,
is_on_fn={"connected": True, "disconnected": False}.get,
),
TibberBinarySensorEntityDescription(
key="charging.status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
is_on_fn={"charging": True, "idle": False}.get,
),
TibberBinarySensorEntityDescription(
key="onOff",
device_class=BinarySensorDeviceClass.POWER,
is_on_fn={"on": True, "off": False}.get,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: TibberConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Tibber binary sensors."""
coordinator = entry.runtime_data.data_api_coordinator
assert coordinator is not None
entities: list[TibberDataAPIBinarySensor] = []
api_binary_sensors = {sensor.key: sensor for sensor in DATA_API_BINARY_SENSORS}
for device in coordinator.data.values():
for sensor in device.sensors:
description: TibberBinarySensorEntityDescription | None = (
api_binary_sensors.get(sensor.id)
)
if description is None:
continue
entities.append(TibberDataAPIBinarySensor(coordinator, device, description))
async_add_entities(entities)
class TibberDataAPIBinarySensor(
CoordinatorEntity[TibberDataAPICoordinator], BinarySensorEntity
):
"""Representation of a Tibber Data API binary sensor."""
_attr_has_entity_name = True
entity_description: TibberBinarySensorEntityDescription
def __init__(
self,
coordinator: TibberDataAPICoordinator,
device: TibberDevice,
entity_description: TibberBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self._device_id: str = device.id
self.entity_description = entity_description
self._attr_unique_id = f"{device.external_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.external_id)},
name=device.name,
manufacturer=device.brand,
model=device.model,
)
@property
def available(self) -> bool:
return super().available and self._device_id in self.coordinator.sensors_by_device
@property
def device(self) -> dict[str, tibber.data_api.Sensor]:
return self.coordinator.sensors_by_device[self._device_id]
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
return self.entity_description.is_on_fn(self.device[self.entity_description.key])

View File

@@ -430,6 +430,9 @@ def _setup_data_api_sensors(
for sensor in device.sensors:
description: SensorEntityDescription | None = api_sensors.get(sensor.id)
if description is None:
_LOGGER.debug(
"Sensor %s not found in DATA_API_SENSORS, skipping", sensor
)
continue
entities.append(TibberDataAPISensor(coordinator, device, description))
async_add_entities(entities)

View File

@@ -16,7 +16,7 @@ from homeassistant.const import Platform
if TYPE_CHECKING:
# InferenceResult is available only from astroid >= 2.12.0
# pre-commit should still work on out of date environments
# prek should still work on out of date environments
from astroid.typing import InferenceResult
_COMMON_ARGUMENTS: dict[str, list[str]] = {

View File

@@ -15,7 +15,7 @@ librt==0.2.1
license-expression==30.4.3
mock-open==1.4.0
mypy-dev==1.19.0a4
pre-commit==4.2.0
prek==0.2.26
pydantic==2.12.2
pylint==4.0.1
pylint-per-file-ignores==1.4.0

View File

@@ -427,7 +427,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
if config.action == "generate" and manifests_resorted:
subprocess.run(
[
"pre-commit",
"prek",
"run",
"--hook-stage",
"manual",

View File

@@ -15,7 +15,7 @@ printf "%s\n" $files
echo "=============="
echo "LINT with ruff"
echo "=============="
pre-commit run ruff-check --files $files
prek run ruff-check --files $files
echo "================"
echo "LINT with pylint"
echo "================"

View File

@@ -119,7 +119,7 @@ async def pylint(files):
async def ruff(files):
"""Exec ruff."""
_, log = await async_exec("pre-commit", "run", "ruff", "--files", *files)
_, log = await async_exec("prek", "run", "ruff", "--files", *files)
res = []
for line in log.splitlines():
line = line.split(":")

View File

@@ -31,7 +31,7 @@ fi
script/bootstrap
pre-commit install
prek install
hass --script ensure_config -c config

View File

@@ -1,150 +0,0 @@
"""Tests for the Tibber binary sensors."""
from __future__ import annotations
from unittest.mock import AsyncMock
import tibber
from homeassistant.components.recorder import Recorder
from homeassistant.components.tibber.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
def create_tibber_device_with_binary_sensors(
device_id: str = "device-id",
external_id: str = "external-id",
name: str = "Test Device",
brand: str = "Tibber",
model: str = "Gen1",
connector_status: str | None = "connected",
charging_status: str | None = "charging",
device_status: str | None = "on",
home_id: str = "home-id",
) -> tibber.data_api.TibberDevice:
"""Create a fake Tibber Data API device with binary sensor capabilities."""
device_data = {
"id": device_id,
"externalId": external_id,
"info": {
"name": name,
"brand": brand,
"model": model,
},
"capabilities": [
{
"id": "connector.status",
"value": connector_status,
"description": "Connector status",
"unit": "",
},
{
"id": "charging.status",
"value": charging_status,
"description": "Charging status",
"unit": "",
},
{
"id": "onOff",
"value": device_status,
"description": "Device status",
"unit": "",
},
],
}
return tibber.data_api.TibberDevice(device_data, home_id=home_id)
async def test_binary_sensors_are_created(
recorder_mock: Recorder,
hass: HomeAssistant,
config_entry: MockConfigEntry,
data_api_client_mock: AsyncMock,
setup_credentials: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Ensure binary sensors are created from Data API devices."""
device = create_tibber_device_with_binary_sensors()
data_api_client_mock.get_all_devices = AsyncMock(return_value={"device-id": device})
data_api_client_mock.update_devices = AsyncMock(return_value={"device-id": device})
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
connector_unique_id = "external-id_connector.status"
connector_entity_id = entity_registry.async_get_entity_id(
"binary_sensor", DOMAIN, connector_unique_id
)
assert connector_entity_id is not None
state = hass.states.get(connector_entity_id)
assert state is not None
assert state.state == "on"
charging_unique_id = "external-id_charging.status"
charging_entity_id = entity_registry.async_get_entity_id(
"binary_sensor", DOMAIN, charging_unique_id
)
assert charging_entity_id is not None
state = hass.states.get(charging_entity_id)
assert state is not None
assert state.state == "on"
device_unique_id = "external-id_onOff"
device_entity_id = entity_registry.async_get_entity_id(
"binary_sensor", DOMAIN, device_unique_id
)
assert device_entity_id is not None
state = hass.states.get(device_entity_id)
assert state is not None
assert state.state == "on"
async def test_device_status_on(
recorder_mock: Recorder,
hass: HomeAssistant,
config_entry: MockConfigEntry,
data_api_client_mock: AsyncMock,
setup_credentials: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device status on state."""
device = create_tibber_device_with_binary_sensors(device_status="on")
data_api_client_mock.get_all_devices = AsyncMock(return_value={"device-id": device})
data_api_client_mock.update_devices = AsyncMock(return_value={"device-id": device})
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
unique_id = "external-id_onOff"
entity_id = entity_registry.async_get_entity_id("binary_sensor", DOMAIN, unique_id)
assert entity_id is not None
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "on"
async def test_device_status_off(
recorder_mock: Recorder,
hass: HomeAssistant,
config_entry: MockConfigEntry,
data_api_client_mock: AsyncMock,
setup_credentials: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device status off state."""
device = create_tibber_device_with_binary_sensors(device_status="off")
data_api_client_mock.get_all_devices = AsyncMock(return_value={"device-id": device})
data_api_client_mock.update_devices = AsyncMock(return_value={"device-id": device})
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
unique_id = "external-id_onOff"
entity_id = entity_registry.async_get_entity_id("binary_sensor", DOMAIN, unique_id)
assert entity_id is not None
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "off"