mirror of
https://github.com/home-assistant/core.git
synced 2026-03-17 16:32:04 +01:00
Compare commits
3 Commits
gha-builde
...
deduplicat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41676011d3 | ||
|
|
b643e06626 | ||
|
|
a2e1b9e474 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
226
.github/workflows/builder.yml
vendored
226
.github/workflows/builder.yml
vendored
@@ -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
1
Dockerfile
generated
@@ -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/"
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
10
machine/build.yaml
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user