Compare commits

..

3 Commits

Author SHA1 Message Date
Erik
41676011d3 Revert unnecessary changes in _parametrize_condition_states 2026-03-17 16:26:41 +01:00
Erik
b643e06626 Revert change to toggle entity tests 2026-03-17 16:22:56 +01:00
Erik
a2e1b9e474 Deduplicate tests testing conditions in mode any 2026-03-17 15:54:25 +01:00
49 changed files with 509 additions and 1001 deletions

1
.gitattributes vendored
View File

@@ -16,7 +16,6 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true

View File

@@ -35,7 +35,6 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -75,8 +74,43 @@ jobs:
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
permissions:
contents: read # To check out the repository
packages: write # To push to GHCR
id-token: write # For cosign signing
strategy:
fail-fast: false
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download nightly wheels of frontend
if: steps.version.outputs.channel == 'dev'
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -87,7 +121,7 @@ jobs:
name: wheels
- name: Download nightly wheels of intents
if: steps.version.outputs.channel == 'dev'
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -97,12 +131,18 @@ jobs:
workflow_conclusion: success
name: package
- name: Set up Python
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
- name: Adjust nightly version
if: steps.version.outputs.channel == 'dev'
if: needs.init.outputs.channel == 'dev'
shell: bash
env:
UV_PRERELEASE: allow
VERSION: ${{ steps.version.outputs.version }}
VERSION: ${{ needs.init.outputs.version }}
run: |
python3 -m pip install "$(grep '^uv' < requirements.txt)"
uv pip install packaging tomli
@@ -140,72 +180,92 @@ jobs:
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
fi
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Write meta info file
shell: bash
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Upload build context overlay
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
name: build-context
if-no-files-found: ignore
path: |
homeassistant/components/*/translations/
rootfs/OFFICIAL_IMAGE
home_assistant_frontend-*.whl
home_assistant_intents-*.whl
homeassistant/const.py
homeassistant/components/frontend/manifest.json
homeassistant/components/conversation/manifest.json
homeassistant/package_constraints.txt
requirements_all.txt
requirements.txt
pyproject.toml
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
permissions:
contents: read # To check out the repository
packages: write # To push to GHCR
id-token: write # For cosign signing
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
os: ubuntu-24.04
- arch: aarch64
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
persist-credentials: false
cosign-release: "v2.5.3"
- name: Download build context overlay
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: build-context
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build variables
id: vars
shell: bash
env:
ARCH: ${{ matrix.arch }}
run: |
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
env:
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${BASE_IMAGE}"
- name: Verify cache image signature
id: cache
continue-on-error: true
env:
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${CACHE_IMAGE}"
- name: Build base image
uses: home-assistant/builder/actions/build-image@gha-builder # zizmor: ignore[unpinned-uses]
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: .
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
image-tags: ${{ needs.init.outputs.version }}
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
version: ${{ needs.init.outputs.version }}
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.init.outputs.version }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -254,38 +314,35 @@ jobs:
with:
persist-credentials: false
- name: Compute extra tags
id: tags
shell: bash
- name: Set build additional args
env:
VERSION: ${{ needs.init.outputs.version }}
run: |
# Create general tags
if [[ "${VERSION}" =~ d ]]; then
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${VERSION}" =~ b ]]; then
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Build machine image
uses: home-assistant/builder/actions/build-image@gha-builder # zizmor: ignore[unpinned-uses]
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: machine/
cosign-base-identity: "https://github.com/home-assistant/core/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
file: machine/${{ matrix.machine }}
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
image-tags: |
${{ needs.init.outputs.version }}
${{ steps.tags.outputs.extra_tags }}
push: true
version: ${{ needs.init.outputs.version }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
publish_ha:
name: Publish version files
@@ -485,10 +542,15 @@ jobs:
with:
python-version-file: ".python-version"
- name: Download build context overlay
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: build-context
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Build package
shell: bash

1
Dockerfile generated
View File

@@ -10,6 +10,7 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"

View File

@@ -89,18 +89,18 @@
"step": {
"advanced": {
"data": {
"api_key": "API token",
"api_key": "API Token",
"api_user": "User ID",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "API token of the Habitica account",
"api_key": "API Token of the Habitica account",
"api_user": "User ID of your Habitica account",
"url": "URL of the Habitica installation to connect to. Defaults to `{default_url}`",
"verify_ssl": "Enable SSL certificate verification for secure connections. Disable only if connecting to a Habitica instance using a self-signed certificate"
},
"description": "You can retrieve your 'User ID' and 'API token' from [**Settings -> Site Data**]({site_data}) on Habitica or the instance you want to connect to",
"description": "You can retrieve your `User ID` and `API Token` from [**Settings -> Site Data**]({site_data}) on Habitica or the instance you want to connect to",
"title": "[%key:component::habitica::config::step::user::menu_options::advanced%]"
},
"login": {
@@ -126,7 +126,7 @@
"api_key": "[%key:component::habitica::config::step::advanced::data_description::api_key%]"
},
"description": "Enter your new API token below. You can find it in Habitica under 'Settings -> Site Data'",
"name": "Re-authorize via API token"
"name": "Re-authorize via API Token"
},
"reauth_login": {
"data": {

View File

@@ -351,6 +351,26 @@ class MoldIndicator(SensorEntity):
return self._get_value_from_state(state, validate_humidity)
async def async_update(self) -> None:
"""Calculate latest state."""
_LOGGER.debug("Update state for %s", self.entity_id)
# check all sensors
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
self._attr_available = False
self._dewpoint = None
self._crit_temp = None
return
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
if self._attr_native_value is None:
self._attr_available = False
self._dewpoint = None
self._crit_temp = None
else:
self._attr_available = True
def _calc_dewpoint(self) -> None:
"""Calculate the dewpoint for the indoor air."""
# Use magnus approximation to calculate the dew point

View File

@@ -105,9 +105,6 @@
"robot_cleaner_driving_mode": {
"default": "mdi:car-cog"
},
"robot_cleaner_sound_mode": {
"default": "mdi:bell-cog"
},
"robot_cleaner_water_spray_level": {
"default": "mdi:spray-bottle"
},

View File

@@ -26,12 +26,6 @@ LAMP_TO_HA = {
"off": "off",
}
SOUND_MODE_TO_HA = {
"voice": "voice",
"beep": "tone",
"mute": "mute",
}
DRIVING_MODE_TO_HA = {
"areaThenWalls": "area_then_walls",
"wallFirst": "walls_first",
@@ -250,16 +244,6 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
entity_category=EntityCategory.CONFIG,
value_is_integer=True,
),
Capability.SAMSUNG_CE_ROBOT_CLEANER_SYSTEM_SOUND_MODE: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_SYSTEM_SOUND_MODE,
translation_key="robot_cleaner_sound_mode",
options_attribute=Attribute.SUPPORTED_SOUND_MODES,
status_attribute=Attribute.SOUND_MODE,
command=Command.SET_SOUND_MODE,
options_map=SOUND_MODE_TO_HA,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
Capability.SAMSUNG_CE_ROBOT_CLEANER_CLEANING_TYPE: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_CLEANING_TYPE,
translation_key="robot_cleaner_cleaning_type",

View File

@@ -254,14 +254,6 @@
"walls_first": "Walls first"
}
},
"robot_cleaner_sound_mode": {
"name": "Sound mode",
"state": {
"mute": "Mute",
"tone": "Tone",
"voice": "Voice"
}
},
"robot_cleaner_water_spray_level": {
"name": "Water level",
"state": {

View File

@@ -195,22 +195,9 @@ class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
if not isinstance(data, dict):
LOGGER.debug(
"%s got unexpected live status response type: %s",
self.name,
type(data).__name__,
)
return self.data
# Convert Wall Connectors from array to dict
wall_connectors = data.get("wall_connectors")
if not isinstance(wall_connectors, list):
wall_connectors = []
data["wall_connectors"] = {
wc["din"]: wc
for wc in wall_connectors
if isinstance(wc, dict) and "din" in wc
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
}
self.updated_once = True

View File

@@ -2,10 +2,9 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, TypedDict, cast
from typing import TYPE_CHECKING, cast
from aiohttp.client_exceptions import ClientError
import tibber
@@ -39,58 +38,6 @@ FIVE_YEARS = 5 * 365 * 24
_LOGGER = logging.getLogger(__name__)
class TibberHomeData(TypedDict):
"""Data for a Tibber home used by the price sensor."""
currency: str
price_unit: str
current_price: float | None
current_price_time: datetime | None
intraday_price_ranking: float | None
max_price: float
avg_price: float
min_price: float
off_peak_1: float
peak: float
off_peak_2: float
month_cost: float | None
peak_hour: float | None
peak_hour_time: datetime | None
month_cons: float | None
app_nickname: str | None
grid_company: str | None
estimated_annual_consumption: int | None
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
"""Build TibberHomeData from a TibberHome for the price sensor."""
current_price, last_updated, price_rank = home.current_price_data()
attributes = home.current_attributes()
result: TibberHomeData = {
"currency": home.currency,
"price_unit": home.price_unit,
"current_price": current_price,
"current_price_time": last_updated,
"intraday_price_ranking": price_rank,
"max_price": attributes["max_price"],
"avg_price": attributes["avg_price"],
"min_price": attributes["min_price"],
"off_peak_1": attributes["off_peak_1"],
"peak": attributes["peak"],
"off_peak_2": attributes["off_peak_2"],
"month_cost": home.month_cost,
"peak_hour": home.peak_hour,
"peak_hour_time": home.peak_hour_time,
"month_cons": home.month_cons,
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
"estimated_annual_consumption": home.info["viewer"]["home"][
"meteringPointData"
]["estimatedAnnualConsumption"],
}
return result
class TibberDataCoordinator(DataUpdateCoordinator[None]):
"""Handle Tibber data and insert statistics."""
@@ -110,16 +57,13 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
name=f"Tibber {tibber_connection.name}",
update_interval=timedelta(minutes=20),
)
self._tibber_connection = tibber_connection
async def _async_update_data(self) -> None:
"""Update data via API."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
try:
await tibber_connection.fetch_consumption_data_active_homes()
await tibber_connection.fetch_production_data_active_homes()
await self._tibber_connection.fetch_consumption_data_active_homes()
await self._tibber_connection.fetch_production_data_active_homes()
await self._insert_statistics()
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
@@ -131,10 +75,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async def _insert_statistics(self) -> None:
"""Insert Tibber statistics."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
for home in tibber_connection.get_homes():
for home in self._tibber_connection.get_homes():
sensors: list[tuple[str, bool, str | None, str]] = []
if home.hourly_consumption_data:
sensors.append(
@@ -253,76 +194,6 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async_add_external_statistics(self.hass, metadata, statistics)
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
"""Handle Tibber price data and insert statistics."""
config_entry: TibberConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: TibberConfigEntry,
) -> None:
"""Initialize the price coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN} price",
update_interval=timedelta(minutes=1),
)
def _seconds_until_next_15_minute(self) -> float:
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
now = dt_util.utcnow()
next_minute = ((now.minute // 15) + 1) * 15
if next_minute >= 60:
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
hours=1
)
else:
next_run = now.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
return (next_run - now).total_seconds()
async def _async_update_data(self) -> dict[str, TibberHomeData]:
"""Update data via API and return per-home data for sensors."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
active_homes = tibber_connection.get_homes(only_active=True)
try:
await asyncio.gather(
tibber_connection.fetch_consumption_data_active_homes(),
tibber_connection.fetch_production_data_active_homes(),
)
now = dt_util.now()
homes_to_update = [
home
for home in active_homes
if (
(last_data_timestamp := home.last_data_timestamp) is None
or (last_data_timestamp - now).total_seconds() < 11 * 3600
)
]
if homes_to_update:
await asyncio.gather(
*(home.update_info_and_price_info() for home in homes_to_update)
)
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
except tibber.FatalHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
result = {home.home_id: _build_home_data(home) for home in active_homes}
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
return result
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
"""Fetch and cache Tibber Data API device capabilities."""

View File

@@ -3,8 +3,10 @@
from __future__ import annotations
from collections.abc import Callable
import datetime
from datetime import timedelta
import logging
from random import randrange
from typing import Any
import aiohttp
@@ -40,20 +42,18 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util import dt as dt_util
from homeassistant.util import Throttle, dt as dt_util
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
from .coordinator import (
TibberDataAPICoordinator,
TibberDataCoordinator,
TibberPriceCoordinator,
)
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:currency-usd"
SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
TWENTY_MINUTES = 20 * 60
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
@@ -610,7 +610,6 @@ async def _async_setup_graphql_sensors(
entity_registry = er.async_get(hass)
coordinator: TibberDataCoordinator | None = None
price_coordinator: TibberPriceCoordinator | None = None
entities: list[TibberSensor] = []
for home in tibber_connection.get_homes(only_active=False):
try:
@@ -627,9 +626,7 @@ async def _async_setup_graphql_sensors(
raise PlatformNotReady from err
if home.has_active_subscription:
if price_coordinator is None:
price_coordinator = TibberPriceCoordinator(hass, entry)
entities.append(TibberSensorElPrice(price_coordinator, home))
entities.append(TibberSensorElPrice(home))
if coordinator is None:
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
entities.extend(
@@ -740,21 +737,19 @@ class TibberSensor(SensorEntity):
return device_info
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
class TibberSensorElPrice(TibberSensor):
"""Representation of a Tibber sensor for el price."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_translation_key = "electricity_price"
def __init__(
self,
coordinator: TibberPriceCoordinator,
tibber_home: TibberHome,
) -> None:
def __init__(self, tibber_home: TibberHome) -> None:
"""Initialize the sensor."""
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
super().__init__(tibber_home=tibber_home)
self._last_updated: datetime.datetime | None = None
self._spread_load_constant = randrange(TWENTY_MINUTES)
self._attr_available = False
self._attr_native_unit_of_measurement = tibber_home.price_unit
self._attr_extra_state_attributes = {
"app_nickname": None,
"grid_company": None,
@@ -773,38 +768,51 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
self._device_name = self._home_name
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data
if not data or (
(home_data := data.get(self._tibber_home.home_id)) is None
or (current_price := home_data.get("current_price")) is None
async def async_update(self) -> None:
"""Get the latest data and updates the states."""
now = dt_util.now()
if (
not self._tibber_home.last_data_timestamp
or (self._tibber_home.last_data_timestamp - now).total_seconds()
< 10 * 3600 - self._spread_load_constant
or not self.available
):
_LOGGER.debug("Asking for new data")
await self._fetch_data()
elif (
self._tibber_home.price_total
and self._last_updated
and self._last_updated.hour == now.hour
and now - self._last_updated < timedelta(minutes=15)
and self._tibber_home.last_data_timestamp
):
self._attr_available = False
self.async_write_ha_state()
return
self._attr_native_unit_of_measurement = home_data.get(
"price_unit", self._tibber_home.price_unit
)
self._attr_native_value = current_price
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
"intraday_price_ranking"
)
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
self._attr_extra_state_attributes["peak"] = home_data["peak"]
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
"estimated_annual_consumption"
res = self._tibber_home.current_price_data()
self._attr_native_value, self._last_updated, price_rank = res
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
attrs = self._tibber_home.current_attributes()
self._attr_extra_state_attributes.update(attrs)
self._attr_available = self._attr_native_value is not None
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self) -> None:
_LOGGER.debug("Fetching data")
try:
await self._tibber_home.update_info_and_price_info()
except TimeoutError, aiohttp.ClientError:
return
data = self._tibber_home.info["viewer"]["home"]
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
"gridCompany"
]
self._attr_available = True
self.async_write_ha_state()
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
"meteringPointData"
]["estimatedAnnualConsumption"]
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Generator
import contextlib
import logging
from typing import Any
from pywemo.exceptions import ActionException
@@ -64,20 +64,23 @@ class WemoEntity(CoordinatorEntity[DeviceCoordinator]):
"""Return the device info."""
return self._device_info
async def _async_wemo_call(self, message: str, action: Callable[[], Any]) -> None:
"""Run a WeMo device action in the executor and update listeners.
@contextlib.contextmanager
def _wemo_call_wrapper(self, message: str) -> Generator[None]:
"""Wrap calls to the device that change its state.
Handles errors from the device and ensures all entities sharing the
same coordinator are aware of updates to the device state.
1. Takes care of making available=False when communications with the
device fails.
2. Ensures all entities sharing the same coordinator are aware of
updates to the device state.
"""
try:
await self.hass.async_add_executor_job(action)
yield
except ActionException as err:
_LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
self.coordinator.last_exception = err
self.coordinator.last_update_success = False
self.coordinator.last_update_success = False # Used for self.available.
finally:
self.coordinator.async_update_listeners()
self.hass.add_job(self.coordinator.async_update_listeners)
class WemoBinaryStateEntity(WemoEntity):

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from datetime import timedelta
import functools as ft
import math
from typing import Any
@@ -61,16 +60,14 @@ async def async_setup_entry(
platform = entity_platform.async_get_current_platform()
# This will call WemoHumidifier.async_set_humidity(target_humidity=VALUE)
# This will call WemoHumidifier.set_humidity(target_humidity=VALUE)
platform.async_register_entity_service(
SERVICE_SET_HUMIDITY,
SET_HUMIDITY_SCHEMA,
WemoHumidifier.async_set_humidity.__name__,
SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, WemoHumidifier.set_humidity.__name__
)
# This will call WemoHumidifier.async_reset_filter_life()
# This will call WemoHumidifier.reset_filter_life()
platform.async_register_entity_service(
SERVICE_RESET_FILTER_LIFE, None, WemoHumidifier.async_reset_filter_life.__name__
SERVICE_RESET_FILTER_LIFE, None, WemoHumidifier.reset_filter_life.__name__
)
@@ -127,26 +124,25 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
self._last_fan_on_mode = self.wemo.fan_mode
super()._handle_coordinator_update()
async def async_turn_on(
def turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the fan on."""
await self._async_set_percentage(percentage)
self._set_percentage(percentage)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self._async_wemo_call(
"turn off", ft.partial(self.wemo.set_state, FanMode.Off)
)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
with self._wemo_call_wrapper("turn off"):
self.wemo.set_state(FanMode.Off)
async def async_set_percentage(self, percentage: int) -> None:
def set_percentage(self, percentage: int) -> None:
"""Set the fan_mode of the Humidifier."""
await self._async_set_percentage(percentage)
self._set_percentage(percentage)
async def _async_set_percentage(self, percentage: int | None) -> None:
def _set_percentage(self, percentage: int | None) -> None:
if percentage is None:
named_speed = self._last_fan_on_mode
elif percentage == 0:
@@ -156,11 +152,10 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
)
await self._async_wemo_call(
"set speed", ft.partial(self.wemo.set_state, named_speed)
)
with self._wemo_call_wrapper("set speed"):
self.wemo.set_state(named_speed)
async def async_set_humidity(self, target_humidity: float) -> None:
def set_humidity(self, target_humidity: float) -> None:
"""Set the target humidity level for the Humidifier."""
if target_humidity < 50:
pywemo_humidity = DesiredHumidity.FortyFivePercent
@@ -173,10 +168,10 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
elif target_humidity >= 100:
pywemo_humidity = DesiredHumidity.OneHundredPercent
await self._async_wemo_call(
"set humidity", ft.partial(self.wemo.set_humidity, pywemo_humidity)
)
with self._wemo_call_wrapper("set humidity"):
self.wemo.set_humidity(pywemo_humidity)
async def async_reset_filter_life(self) -> None:
def reset_filter_life(self) -> None:
"""Reset the filter life to 100%."""
await self._async_wemo_call("reset filter life", self.wemo.reset_filter_life)
with self._wemo_call_wrapper("reset filter life"):
self.wemo.reset_filter_life()

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import functools as ft
from typing import Any, cast
from pywemo import Bridge, BridgeLight, Dimmer
@@ -167,7 +166,7 @@ class WemoLight(WemoEntity, LightEntity):
"""Return true if device is on."""
return self.light.state.get("onoff", WEMO_OFF) != WEMO_OFF
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
xy_color = None
@@ -185,7 +184,7 @@ class WemoLight(WemoEntity, LightEntity):
"force_update": False,
}
def _turn_on() -> None:
with self._wemo_call_wrapper("turn on"):
if xy_color is not None:
self.light.set_color(xy_color, transition=transition_time)
@@ -196,14 +195,12 @@ class WemoLight(WemoEntity, LightEntity):
self.light.turn_on(**turn_on_kwargs)
await self._async_wemo_call("turn on", _turn_on)
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition_time = int(kwargs.get(ATTR_TRANSITION, 0))
await self._async_wemo_call(
"turn off", ft.partial(self.light.turn_off, transition=transition_time)
)
with self._wemo_call_wrapper("turn off"):
self.light.turn_off(transition=transition_time)
class WemoDimmer(WemoBinaryStateEntity, LightEntity):
@@ -219,19 +216,20 @@ class WemoDimmer(WemoBinaryStateEntity, LightEntity):
wemo_brightness: int = self.wemo.get_brightness()
return int((wemo_brightness * 255) / 100)
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the dimmer on."""
# Wemo dimmer switches use a range of [0, 100] to control
# brightness. Level 255 might mean to set it to previous value
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
brightness = int((brightness / 255) * 100)
await self._async_wemo_call(
"set brightness", ft.partial(self.wemo.set_brightness, brightness)
)
with self._wemo_call_wrapper("set brightness"):
self.wemo.set_brightness(brightness)
else:
await self._async_wemo_call("turn on", self.wemo.on)
with self._wemo_call_wrapper("turn on"):
self.wemo.on()
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn the dimmer off."""
await self._async_wemo_call("turn off", self.wemo.off)
with self._wemo_call_wrapper("turn off"):
self.wemo.off()

View File

@@ -119,10 +119,12 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity):
return "mdi:coffee"
return None
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self._async_wemo_call("turn on", self.wemo.on)
with self._wemo_call_wrapper("turn on"):
self.wemo.on()
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self._async_wemo_call("turn off", self.wemo.off)
with self._wemo_call_wrapper("turn off"):
self.wemo.off()

10
machine/build.yaml Normal file
View File

@@ -0,0 +1,10 @@
image: ghcr.io/home-assistant/{machine}-homeassistant
build_from:
aarch64: "ghcr.io/home-assistant/aarch64-homeassistant:"
amd64: "ghcr.io/home-assistant/amd64-homeassistant:"
cosign:
base_identity: https://github.com/home-assistant/core/.*
identity: https://github.com/home-assistant/core/.*
labels:
io.hass.type: core
org.opencontainers.image.source: https://github.com/home-assistant/core

View File

@@ -1,10 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
libva-intel-driver
LABEL io.hass.machine="generic-x86-64"

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="green"
FROM $BUILD_FROM

View File

@@ -1,10 +1,10 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
# NOTE: intel-nuc will be replaced by generic-x86-64. Make sure to apply
# changes in generic-x86-64 as well.
RUN apk --no-cache add \
libva-intel-driver
LABEL io.hass.machine="intel-nuc"

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="khadas-vim3"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="odroid-c2"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="odroid-c4"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="odroid-m1"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="odroid-n2"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="qemuarm-64"
FROM $BUILD_FROM

View File

@@ -1,7 +1,4 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
LABEL io.hass.machine="qemux86-64"
FROM $BUILD_FROM

View File

@@ -1,10 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-utils
LABEL io.hass.machine="raspberrypi3-64"
raspberrypi-utils

View File

@@ -1,10 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-utils
LABEL io.hass.machine="raspberrypi4-64"
raspberrypi-utils

View File

@@ -1,10 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-utils
LABEL io.hass.machine="raspberrypi5-64"
raspberrypi-utils

View File

@@ -1,10 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-utils
LABEL io.hass.machine="yellow"
raspberrypi-utils

View File

@@ -25,6 +25,7 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"
@@ -76,59 +77,6 @@ RUN \
WORKDIR /config
"""
@dataclass(frozen=True)
class _MachineConfig:
"""Machine-specific Dockerfile configuration."""
arch: str
packages: tuple[str, ...] = ()
_MACHINES = {
"generic-x86-64": _MachineConfig(arch="amd64", packages=("libva-intel-driver",)),
"green": _MachineConfig(arch="aarch64"),
"intel-nuc": _MachineConfig(arch="amd64", packages=("libva-intel-driver",)),
"khadas-vim3": _MachineConfig(arch="aarch64"),
"odroid-c2": _MachineConfig(arch="aarch64"),
"odroid-c4": _MachineConfig(arch="aarch64"),
"odroid-m1": _MachineConfig(arch="aarch64"),
"odroid-n2": _MachineConfig(arch="aarch64"),
"qemuarm-64": _MachineConfig(arch="aarch64"),
"qemux86-64": _MachineConfig(arch="amd64"),
"raspberrypi3-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi4-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi5-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"yellow": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
}
_MACHINE_DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/{arch}-homeassistant:latest
FROM ${{BUILD_FROM}}
{extra_packages}
LABEL io.hass.machine="{machine}"
"""
def _generate_machine_dockerfile(
machine_name: str, machine_config: _MachineConfig
) -> str:
"""Generate a machine Dockerfile from configuration."""
if machine_config.packages:
pkg_lines = " \\\n ".join(machine_config.packages)
extra_packages = f"\nRUN apk --no-cache add \\\n {pkg_lines}\n"
else:
extra_packages = ""
return _MACHINE_DOCKERFILE_TEMPLATE.format(
arch=machine_config.arch,
extra_packages=extra_packages,
machine=machine_name,
)
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
@@ -226,7 +174,7 @@ def _generate_files(config: Config) -> list[File]:
config.root / "requirements_test_pre_commit.txt", {"ruff"}
)
files = [
return [
File(
DOCKERFILE_TEMPLATE.format(
timeout=timeout,
@@ -244,16 +192,6 @@ def _generate_files(config: Config) -> list[File]:
),
]
for machine_name, machine_config in sorted(_MACHINES.items()):
files.append(
File(
_generate_machine_dockerfile(machine_name, machine_config),
config.root / "machine" / machine_name,
)
)
return files
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate dockerfile."""

View File

@@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -129,32 +130,17 @@ async def test_alarm_control_panel_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the alarm_control_panel state condition with the 'any' behavior."""
other_entity_ids = set(target_alarm_control_panels["included"]) - {entity_id}
# Set all alarm_control_panels, including the tested alarm_control_panel, to the initial state
for eid in target_alarm_control_panels["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_alarm_control_panels,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other alarm_control_panels also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -83,32 +84,17 @@ async def test_assist_satellite_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the assist satellite state condition with the 'any' behavior."""
other_entity_ids = set(target_assist_satellites["included"]) - {entity_id}
# Set all assist satellites, including the tested one, to the initial state
for eid in target_assist_satellites["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_assist_satellites,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other assist satellites also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -85,32 +86,17 @@ async def test_climate_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate state condition with the 'any' behavior."""
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other climates also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -214,32 +200,17 @@ async def test_climate_attribute_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate attribute condition with the 'any' behavior."""
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other climates also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -864,6 +864,51 @@ async def assert_trigger_gated_by_labs_flag(
) in caplog.text
async def assert_condition_behavior_any(
hass: HomeAssistant,
*,
target_entities: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test condition with the 'any' behavior."""
other_entity_ids = set(target_entities["included"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
for eid in target_entities["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
condition=condition,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
excluded_state = state["excluded"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
async def assert_trigger_behavior_any(
hass: HomeAssistant,
*,

View File

@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
@@ -100,38 +101,17 @@ async def test_cover_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test cover condition with the 'any' behavior."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
excluded_state = state["excluded"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
@@ -70,32 +71,17 @@ async def test_device_tracker_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the device tracker state condition with the 'any' behavior."""
other_entity_ids = set(target_device_trackers["included"]) - {entity_id}
# Set all device trackers, including the tested one, to the initial state
for eid in target_device_trackers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_device_trackers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other device trackers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
@@ -73,32 +74,17 @@ async def test_humidifier_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier state condition with the 'any' behavior."""
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other humidifiers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -190,32 +176,17 @@ async def test_humidifier_attribute_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier attribute condition with the 'any' behavior."""
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other humidifiers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -89,32 +90,17 @@ async def test_lawn_mower_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the lawn mower state condition with the 'any' behavior."""
other_entity_ids = set(target_lawn_mowers["included"]) - {entity_id}
# Set all lawn mowers, including the tested lawn mower, to the initial state
for eid in target_lawn_mowers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_lawn_mowers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other lawn mowers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -83,32 +84,17 @@ async def test_lock_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the lock state condition with the 'any' behavior."""
other_entity_ids = set(target_locks["included"]) - {entity_id}
# Set all locks, including the tested lock, to the initial state
for eid in target_locks["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_locks,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other locks also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -101,32 +102,17 @@ async def test_media_player_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the media player state condition with the 'any' behavior."""
other_entity_ids = set(target_media_players["included"]) - {entity_id}
# Set all media players, including the tested media player, to the initial state
for eid in target_media_players["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_media_players,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other media players also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
@@ -70,32 +71,17 @@ async def test_person_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the person state condition with the 'any' behavior."""
other_entity_ids = set(target_persons["included"]) - {entity_id}
# Set all persons, including the tested person, to the initial state
for eid in target_persons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_persons,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other persons also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -725,66 +725,6 @@
'state': 'medium',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'mute',
'tone',
'voice',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.robot_vacuum_sound_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound mode',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_sound_mode',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.robotCleanerSystemSoundMode_soundMode_soundMode',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Sound mode',
'options': list([
'mute',
'tone',
'voice',
]),
}),
'context': <ANY>,
'entity_id': 'select.robot_vacuum_sound_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'tone',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_water_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -41,7 +41,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import setup_platform
from .conftest import create_config_entry
from .const import LIVE_STATUS, VEHICLE_ASLEEP, VEHICLE_DATA_ALT
from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -352,42 +352,6 @@ async def test_energy_live_refresh_error(
assert normal_config_entry.state is state
async def test_energy_live_refresh_bad_response(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_live_status: AsyncMock,
) -> None:
"""Test coordinator refresh with malformed live status payload."""
bad_live_status = deepcopy(LIVE_STATUS)
bad_live_status["response"] = "site data is unavailable"
mock_live_status.side_effect = None
mock_live_status.return_value = bad_live_status
await setup_platform(hass, normal_config_entry)
assert normal_config_entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("sensor.test_battery_level"))
assert state.state != "unavailable"
async def test_energy_live_refresh_bad_wall_connectors(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_live_status: AsyncMock,
) -> None:
"""Test coordinator refresh with malformed wall connector payload."""
bad_live_status = deepcopy(LIVE_STATUS)
bad_live_status["response"]["wall_connectors"] = "site data is unavailable"
mock_live_status.side_effect = None
mock_live_status.return_value = bad_live_status
await setup_platform(hass, normal_config_entry)
assert normal_config_entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("sensor.test_battery_level"))
assert state.state != "unavailable"
# Test Energy Site Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_site_refresh_error(

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock
import pytest
@@ -10,97 +10,12 @@ 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 homeassistant.helpers.entity_component import async_update_entity
from homeassistant.util import dt as dt_util
from .conftest import create_tibber_device
from tests.common import MockConfigEntry
def _create_home(*, current_price: float | None = 1.25) -> MagicMock:
"""Create a mocked Tibber home with an active subscription."""
home = MagicMock()
home.home_id = "home-id"
home.name = "Home"
home.currency = "NOK"
home.price_unit = "NOK/kWh"
home.has_active_subscription = True
home.has_real_time_consumption = False
home.last_data_timestamp = None
home.update_info = AsyncMock(return_value=None)
home.update_info_and_price_info = AsyncMock(return_value=None)
home.current_price_data = MagicMock(
return_value=(current_price, dt_util.utcnow(), 0.4)
)
home.current_attributes = MagicMock(
return_value={
"max_price": 1.8,
"avg_price": 1.2,
"min_price": 0.8,
"off_peak_1": 0.9,
"peak": 1.7,
"off_peak_2": 1.0,
}
)
home.month_cost = 111.1
home.peak_hour = 2.5
home.peak_hour_time = dt_util.utcnow()
home.month_cons = 222.2
home.hourly_consumption_data = []
home.hourly_production_data = []
home.info = {
"viewer": {
"home": {
"appNickname": "Home",
"address": {"address1": "Street 1"},
"meteringPointData": {
"gridCompany": "GridCo",
"estimatedAnnualConsumption": 12000,
},
}
}
}
return home
async def test_price_sensor_state_unit_and_attributes(
recorder_mock: Recorder,
hass: HomeAssistant,
config_entry: MockConfigEntry,
tibber_mock: MagicMock,
setup_credentials: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Test price sensor state and attributes."""
home = _create_home(current_price=1.25)
tibber_mock.get_homes.return_value = [home]
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, home.home_id)
assert entity_id is not None
await async_update_entity(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert float(state.state) == 1.25
assert state.attributes["unit_of_measurement"] == "NOK/kWh"
assert state.attributes["app_nickname"] == "Home"
assert state.attributes["grid_company"] == "GridCo"
assert state.attributes["estimated_annual_consumption"] == 12000
assert state.attributes["intraday_price_ranking"] == 0.4
assert state.attributes["max_price"] == 1.8
assert state.attributes["avg_price"] == 1.2
assert state.attributes["min_price"] == 0.8
assert state.attributes["off_peak_1"] == 0.9
assert state.attributes["peak"] == 1.7
assert state.attributes["off_peak_2"] == 1.0
async def test_data_api_sensors_are_created(
recorder_mock: Recorder,
hass: HomeAssistant,

View File

@@ -24,10 +24,6 @@ async def test_async_setup_entry(
tibber_connection.fetch_production_data_active_homes.return_value = None
tibber_connection.get_homes = mock_get_homes
runtime_data = AsyncMock()
runtime_data.async_get_client.return_value = tibber_connection
config_entry.runtime_data = runtime_data
coordinator = TibberDataCoordinator(hass, config_entry, tibber_connection)
await coordinator._async_update_data()
await async_wait_recording_done(hass)

View File

@@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
@@ -89,32 +90,17 @@ async def test_vacuum_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the vacuum state condition with the 'any' behavior."""
other_entity_ids = set(target_vacuums["included"]) - {entity_id}
# Set all vacuums, including the tested vacuum, to the initial state
for eid in target_vacuums["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
await assert_condition_behavior_any(
hass,
target_entities=target_vacuums,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
target=condition_target_config,
behavior="any",
condition_options=condition_options,
states=states,
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other vacuums also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -146,6 +146,11 @@ async def test_avaliable_after_update(
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
blocking=True,
)
# _wemo_call_wrapper schedules async_update_listeners via hass.add_job
# from the executor thread, which goes through two levels of call_soon
# before the entity state is written.
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE

View File

@@ -73,6 +73,11 @@ async def test_turn_on_brightness(
{ATTR_ENTITY_ID: [wemo_entity.entity_id], ATTR_BRIGHTNESS: 204},
blocking=True,
)
# _wemo_call_wrapper schedules async_update_listeners via hass.add_job
# from the executor thread, which goes through two levels of call_soon
# before the entity state is written.
await hass.async_block_till_done()
await hass.async_block_till_done()
pywemo_device.set_brightness.assert_called_once_with(80)
states = hass.states.get(wemo_entity.entity_id)