Compare commits

..

6 Commits

Author SHA1 Message Date
Martin Hjelmare
5a0abd98e1 Merge branch 'dev' into state_attr_translated 2026-03-18 11:36:00 +01:00
Paul Bottein
bfdab0a219 Remove unused mock 2026-03-17 17:18:33 +01:00
Paul Bottein
1af85be44f Parametrize tests 2026-03-17 17:16:47 +01:00
Paul Bottein
1e7210c7e0 Fix import order 2026-03-17 17:16:47 +01:00
Copilot
551d52dd3c Fix state_attr_translated to preserve original type for non-string attributes (#165320)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-03-17 17:16:47 +01:00
Paul Bottein
29d3e71627 Add state_attr_translated template filter and function 2026-03-17 17:16:46 +01:00
104 changed files with 1534 additions and 4016 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
@@ -101,7 +100,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-24.04
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
@@ -196,20 +195,77 @@ jobs:
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Build base image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
- 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-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
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 }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- 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
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
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
@@ -258,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@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
- 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

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

@@ -125,8 +125,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"device_tracker",
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"lawn_mower",
"light",
@@ -138,7 +136,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"siren",
"switch",
"vacuum",
"window",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
@@ -165,7 +162,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"remote",
"scene",
"schedule",
"select",
"siren",
"switch",
"text",

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==67"],
"requirements": ["axis==66"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -246,8 +246,6 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -334,10 +332,8 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size

View File

@@ -66,7 +66,6 @@ class ClementineDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY
)
_attr_volume_step = 0.04
def __init__(self, client, name):
"""Initialize the Clementine device."""
@@ -125,6 +124,16 @@ class ClementineDevice(MediaPlayerEntity):
return None, None
def volume_up(self) -> None:
"""Volume up the media player."""
newvolume = min(self._client.volume + 4, 100)
self._client.set_volume(newvolume)
def volume_down(self) -> None:
"""Volume down media player."""
newvolume = max(self._client.volume - 4, 0)
self._client.set_volume(newvolume)
def mute_volume(self, mute: bool) -> None:
"""Send mute command."""
self._client.set_volume(0)

View File

@@ -1,31 +0,0 @@
"""Provides conditions for garage doors."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_GARAGE_DOOR: dict[str, str] = {
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.GARAGE_DOOR,
COVER_DOMAIN: CoverDeviceClass.GARAGE,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_GARAGE_DOOR
),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GARAGE_DOOR),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for garage doors."""
return CONDITIONS

View File

@@ -1,28 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: garage_door
- domain: cover
device_class: garage
is_open:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: garage_door
- domain: cover
device_class: garage

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:garage"
},
"is_open": {
"condition": "mdi:garage-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:garage"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted garage doors.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted garage doors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more garage doors are closed.",
"fields": {
"behavior": {
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
}
},
"name": "Garage door is closed"
},
"is_open": {
"description": "Tests if one or more garage doors are open.",
"fields": {
"behavior": {
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
}
},
"name": "Garage door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -1,24 +0,0 @@
"""Provides conditions for gates."""
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_GATE: dict[str, str] = {
COVER_DOMAIN: CoverDeviceClass.GATE,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_GATE),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GATE),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for gates."""
return CONDITIONS

View File

@@ -1,24 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: gate
is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: gate

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:gate"
},
"is_open": {
"condition": "mdi:gate-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:gate"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted gates.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted gates to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more gates are closed.",
"fields": {
"behavior": {
"description": "[%key:component::gate::common::condition_behavior_description%]",
"name": "[%key:component::gate::common::condition_behavior_name%]"
}
},
"name": "Gate is closed"
},
"is_open": {
"description": "Tests if one or more gates are open.",
"fields": {
"behavior": {
"description": "[%key:component::gate::common::condition_behavior_description%]",
"name": "[%key:component::gate::common::condition_behavior_name%]"
}
},
"name": "Gate is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -251,7 +251,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
def get_data(
self, entity_description: GrowattSensorEntityDescription
) -> str | int | float | datetime.datetime | datetime.date | None:
) -> str | int | float | None:
"""Get the data."""
variable = entity_description.api_key
api_value = self.data.get(variable)

View File

@@ -1,17 +1,4 @@
{
"entity": {
"sensor": {
"storage_load_consumption_solar_storage": {
"default": "mdi:lightning-bolt"
},
"total_money_today": {
"default": "mdi:cash"
},
"total_money_total": {
"default": "mdi:cash"
}
}
},
"services": {
"read_ac_charge_times": {
"service": "mdi:battery-clock-outline"

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GrowattConfigEntry, GrowattCoordinator
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
_LOGGER = logging.getLogger(__name__)
@@ -26,10 +27,9 @@ PARALLEL_UPDATES = (
@dataclass(frozen=True, kw_only=True)
class GrowattNumberEntityDescription(NumberEntityDescription):
class GrowattNumberEntityDescription(NumberEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt number entity."""
api_key: str
write_key: str | None = None # Parameter ID for writing (if different from api_key)

View File

@@ -45,11 +45,13 @@ rules:
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-device-class:
status: todo
comment: Replace custom precision field with suggested_display_precision to preserve full data granularity.
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt

View File

@@ -2,14 +2,12 @@
from __future__ import annotations
from datetime import date, datetime
import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from ..const import DOMAIN
@@ -101,6 +99,7 @@ class GrowattSensor(CoordinatorEntity[GrowattCoordinator], SensorEntity):
self.entity_description = description
self._attr_unique_id = unique_id
self._attr_icon = "mdi:solar-power"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_id)},
@@ -110,9 +109,15 @@ class GrowattSensor(CoordinatorEntity[GrowattCoordinator], SensorEntity):
)
@property
def native_value(self) -> StateType | date | datetime:
def native_value(self) -> str | int | float | None:
"""Return the state of the sensor."""
return self.coordinator.get_data(self.entity_description)
result = self.coordinator.get_data(self.entity_description)
if (
isinstance(result, (int, float))
and self.entity_description.precision is not None
):
result = round(result, self.entity_description.precision)
return result
@property
def native_unit_of_measurement(self) -> str | None:

View File

@@ -23,7 +23,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_energy_total",
@@ -31,7 +31,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="powerTotal",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
precision=1,
state_class=SensorStateClass.TOTAL,
),
GrowattSensorEntityDescription(
@@ -41,7 +41,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_1",
@@ -50,7 +50,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_1",
@@ -59,7 +59,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_voltage_input_2",
@@ -68,7 +68,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_2",
@@ -77,7 +77,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_2",
@@ -86,7 +86,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_voltage_input_3",
@@ -95,7 +95,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_3",
@@ -104,7 +104,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_3",
@@ -113,7 +113,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_internal_wattage",
@@ -122,7 +122,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_reactive_voltage",
@@ -131,7 +131,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -142,7 +142,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -153,7 +153,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -164,7 +164,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_current_reactive_wattage",
@@ -173,7 +173,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -184,7 +184,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -195,7 +195,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),

View File

@@ -7,11 +7,18 @@ from dataclasses import dataclass
from homeassistant.components.sensor import SensorEntityDescription
@dataclass(frozen=True, kw_only=True)
class GrowattSensorEntityDescription(SensorEntityDescription):
"""Describes Growatt sensor entity."""
@dataclass(frozen=True)
class GrowattRequiredKeysMixin:
"""Mixin for required keys."""
api_key: str
@dataclass(frozen=True)
class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt sensor entity."""
precision: int | None = None
currency: bool = False
previous_value_drop_threshold: float | None = None
never_resets: bool = False

View File

@@ -190,7 +190,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_pv_charging_voltage",
@@ -199,7 +199,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_pv_charging_voltage_2",
@@ -208,7 +208,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_ac_input_frequency_out",
@@ -217,7 +217,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -228,7 +228,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_ac_output_frequency",
@@ -237,7 +237,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -248,7 +248,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_current_1",
@@ -257,7 +257,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_current_2",
@@ -266,7 +266,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_grid_amperage_input",
@@ -275,7 +275,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_grid_out_current",
@@ -284,7 +284,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_battery_voltage",
@@ -293,7 +293,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_load_percentage",
@@ -302,6 +302,6 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
)

View File

@@ -27,7 +27,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total",
@@ -36,7 +36,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -46,7 +46,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -56,7 +56,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_1",
@@ -64,7 +64,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv1",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_1",
@@ -72,7 +72,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv1",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_1",
@@ -81,7 +81,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_2",
@@ -90,7 +90,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -100,7 +100,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_2",
@@ -108,7 +108,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv2",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_2",
@@ -116,7 +116,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv2",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_2",
@@ -125,7 +125,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_3",
@@ -134,7 +134,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -144,7 +144,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_3",
@@ -152,7 +152,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv3",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_3",
@@ -160,7 +160,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv3",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_3",
@@ -169,7 +169,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_4",
@@ -178,7 +178,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -188,7 +188,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_4",
@@ -196,7 +196,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv4",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_4",
@@ -204,7 +204,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv4",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_4",
@@ -213,7 +213,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_solar_generation_today",
@@ -222,7 +222,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_solar_generation_total",
@@ -240,7 +240,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_reactive_voltage",
@@ -248,7 +248,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vacrs",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -258,7 +258,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="fac",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -269,7 +269,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_1",
@@ -277,7 +277,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp1",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -287,7 +287,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp2",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -297,7 +297,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp3",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -307,7 +307,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp4",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -317,7 +317,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp5",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
@@ -471,7 +471,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_pac_to_user_total",
@@ -480,7 +480,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_pac_to_grid_total",
@@ -489,7 +489,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_system_production_today",
@@ -498,7 +498,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_system_production_total",
@@ -508,7 +508,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_self_consumption_today",
@@ -517,7 +517,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_self_consumption_total",
@@ -527,7 +527,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_import_from_grid_today",
@@ -536,7 +536,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_import_from_grid_total",
@@ -546,7 +546,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_batteries_charged_from_grid_today",
@@ -555,7 +555,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_batteries_charged_from_grid_total",
@@ -565,7 +565,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_p_system",
@@ -574,7 +574,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_p_self",
@@ -583,6 +583,6 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
)

View File

@@ -18,6 +18,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GrowattConfigEntry, GrowattCoordinator
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
_LOGGER = logging.getLogger(__name__)
@@ -27,10 +28,9 @@ PARALLEL_UPDATES = (
@dataclass(frozen=True, kw_only=True)
class GrowattSwitchEntityDescription(SwitchEntityDescription):
class GrowattSwitchEntityDescription(SwitchEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt switch entity."""
api_key: str
write_key: str | None = None # Parameter ID for writing (if different from api_key)

View File

@@ -119,6 +119,7 @@ from .coordinator import (
get_core_stats,
get_host_info,
get_info,
get_issues_info,
get_network_info,
get_os_info,
get_store,
@@ -157,6 +158,7 @@ __all__ = [
"get_core_stats",
"get_host_info",
"get_info",
"get_issues_info",
"get_network_info",
"get_os_info",
"get_store",

View File

@@ -132,7 +132,6 @@ ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
ISSUE_KEY_ADDON_DEPRECATED_ARCH = "issue_addon_deprecated_arch_addon"
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
@@ -173,7 +172,6 @@ EXTRA_PLACEHOLDERS = {
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
},
ISSUE_KEY_ADDON_DEPRECATED: HELP_URLS,
ISSUE_KEY_ADDON_DEPRECATED_ARCH: HELP_URLS,
}

View File

@@ -47,7 +47,6 @@ from .const import (
EVENT_SUPPORTED_CHANGED,
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
@@ -91,7 +90,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
}
_LOGGER = logging.getLogger(__name__)
@@ -255,10 +253,9 @@ class SupervisorIssues:
def add_issue(self, issue: Issue) -> None:
"""Add or update an issue in the list. Create or update a repair if necessary."""
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
placeholders: dict[str, str] = {}
if not issue.suggestions and issue.key in EXTRA_PLACEHOLDERS:
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[issue.key].copy()
else:
placeholders = {}
placeholders |= EXTRA_PLACEHOLDERS[issue.key]
if issue.reference:
placeholders[PLACEHOLDER_KEY_REFERENCE] = issue.reference

View File

@@ -21,7 +21,6 @@ from .const import (
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
@@ -65,16 +64,11 @@ class SupervisorIssueRepairFlow(RepairsFlow):
@property
def description_placeholders(self) -> dict[str, str] | None:
"""Get description placeholders for steps."""
if not self.issue:
return None
if self.issue.key in EXTRA_PLACEHOLDERS:
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[self.issue.key].copy()
else:
placeholders = {}
if self.issue.reference:
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
placeholders = {}
if self.issue:
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
if self.issue.reference:
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
return placeholders or None
@@ -238,7 +232,6 @@ async def async_create_fix_flow(
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
}:
return AddonIssueRepairFlow(hass, issue_id)

View File

@@ -85,19 +85,6 @@
},
"title": "Installed app is deprecated"
},
"issue_addon_deprecated_arch_addon": {
"fix_flow": {
"abort": {
"apply_suggestion_fail": "Could not uninstall the app. Check the Supervisor logs for more details."
},
"step": {
"addon_execute_remove": {
"description": "App {addon} only supports architectures and/or machines which are no longer supported by Home Assistant. It will stop working in a future release.\n\nSelecting **Submit** will uninstall this deprecated app. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
}
}
},
"title": "Installed app is built for unsupported architectures and/or machines"
},
"issue_addon_detached_addon_missing": {
"description": "Repository for app {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the Home Assistant Supervisor may not be able to build/download the resources required.\n\nPlease check the [app's documentation]({addon_url}) for installation instructions and add the repository to the store.",
"title": "Missing repository for an installed app"

View File

@@ -22,13 +22,7 @@
"description": "Authenticate against IntelliFire cloud"
},
"pick_cloud_device": {
"data": {
"serial": "Fireplace serial number"
},
"data_description": {
"serial": "Serial number of the fireplace to configure"
},
"description": "Select fireplace by serial number.",
"description": "Select fireplace by serial number:",
"title": "Configure fireplace"
}
}
@@ -165,10 +159,6 @@
"control_mode": "Send commands to",
"read_mode": "Read data from"
},
"data_description": {
"control_mode": "Whether to send fireplace commands via the `Local` or `Cloud` API",
"read_mode": "Whether to read fireplace state via the `Local` or `Cloud` API"
},
"description": "Some users find that their fireplace hardware prioritizes `Cloud` communication and may experience timeouts with `Local` control. If you encounter connectivity issues, try switching to `Cloud` for the affected endpoint.",
"title": "Endpoint selection"
}

View File

@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
ProxmoxVMButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
ProxmoxContainerButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,

View File

@@ -18,7 +18,6 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
]

View File

@@ -1,144 +0,0 @@
"""Support for Renault number entities."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, cast
from renault_api.kamereon.models import KamereonVehicleBatterySocData
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RenaultConfigEntry
from .const import DOMAIN
from .entity import RenaultDataEntity, RenaultDataEntityDescription
# Coordinator is used to centralize the data updates
# but renault servers are unreliable and it's safer to queue action calls
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class RenaultNumberEntityDescription(
NumberEntityDescription, RenaultDataEntityDescription
):
"""Class describing Renault number entities."""
data_key: str
update_fn: Callable[[RenaultNumberEntity, float], Coroutine[Any, Any, None]]
async def _set_charge_limit_min(entity: RenaultNumberEntity, value: float) -> None:
"""Set the minimum SOC.
The target SOC is required to set the minimum SOC, so we need to fetch it first.
"""
if (data := entity.coordinator.data) is None or (
target_soc := data.socTarget
) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="battery_soc_unavailable",
)
await _set_charge_limits(entity, min_soc=round(value), target_soc=target_soc)
async def _set_charge_limit_target(entity: RenaultNumberEntity, value: float) -> None:
"""Set the target SOC.
The minimum SOC is required to set the target SOC, so we need to fetch it first.
"""
if (data := entity.coordinator.data) is None or (min_soc := data.socMin) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="battery_soc_unavailable",
)
await _set_charge_limits(entity, min_soc=min_soc, target_soc=round(value))
async def _set_charge_limits(
entity: RenaultNumberEntity, min_soc: int, target_soc: int
) -> None:
"""Set the minimum and target SOC.
Optimistically update local coordinator data so the new
limits are reflected immediately without a remote refresh,
as Renault servers may still cache old values.
"""
await entity.vehicle.set_battery_soc(min_soc=min_soc, target_soc=target_soc)
entity.coordinator.data.socMin = min_soc
entity.coordinator.data.socTarget = target_soc
entity.coordinator.async_set_updated_data(entity.coordinator.data)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: RenaultConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renault entities from config entry."""
entities: list[RenaultNumberEntity] = [
RenaultNumberEntity(vehicle, description)
for vehicle in config_entry.runtime_data.vehicles.values()
for description in NUMBER_TYPES
if description.coordinator in vehicle.coordinators
]
async_add_entities(entities)
class RenaultNumberEntity(
RenaultDataEntity[KamereonVehicleBatterySocData], NumberEntity
):
"""Mixin for number specific attributes."""
entity_description: RenaultNumberEntityDescription
@property
def native_value(self) -> float | None:
"""Return the entity value to represent the entity state."""
return cast(float | None, self._get_data_attr(self.entity_description.data_key))
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self.entity_description.update_fn(self, value)
NUMBER_TYPES: tuple[RenaultNumberEntityDescription, ...] = (
RenaultNumberEntityDescription(
key="charge_limit_min",
coordinator="battery_soc",
data_key="socMin",
update_fn=_set_charge_limit_min,
device_class=NumberDeviceClass.BATTERY,
native_min_value=15,
native_max_value=45,
native_step=5,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
translation_key="charge_limit_min",
),
RenaultNumberEntityDescription(
key="charge_limit_target",
coordinator="battery_soc",
data_key="socTarget",
update_fn=_set_charge_limit_target,
device_class=NumberDeviceClass.BATTERY,
native_min_value=55,
native_max_value=100,
native_step=5,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
translation_key="charge_limit_target",
),
)

View File

@@ -174,13 +174,6 @@ class RenaultVehicleProxy:
"""Stop vehicle charge."""
return await self._vehicle.set_charge_stop()
@with_error_wrapping
async def set_battery_soc(
self, min_soc: int, target_soc: int
) -> models.KamereonVehicleBatterySocActionData:
"""Set vehicle battery SoC levels."""
return await self._vehicle.set_battery_soc(min=min_soc, target=target_soc)
@with_error_wrapping
async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData:
"""Stop vehicle ac."""
@@ -277,10 +270,4 @@ COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = (
key="pressure",
update_method=lambda x: x.get_tyre_pressure,
),
RenaultCoordinatorDescription(
endpoint="soc-levels",
key="battery_soc",
requires_electricity=True,
update_method=lambda x: x.get_battery_soc,
),
)

View File

@@ -94,14 +94,6 @@
"name": "[%key:common::config_flow::data::location%]"
}
},
"number": {
"charge_limit_min": {
"name": "Minimum charge level"
},
"charge_limit_target": {
"name": "Target charge level"
}
},
"select": {
"charge_mode": {
"name": "Charge mode",
@@ -207,9 +199,6 @@
}
},
"exceptions": {
"battery_soc_unavailable": {
"message": "Battery state of charge data is currently unavailable"
},
"invalid_device_id": {
"message": "No device with ID {device_id} was found"
},

View File

@@ -20,10 +20,5 @@
"select_previous": {
"service": "mdi:format-list-bulleted"
}
},
"triggers": {
"selection_changed": {
"trigger": "mdi:format-list-bulleted"
}
}
}

View File

@@ -76,11 +76,5 @@
"name": "Previous"
}
},
"title": "Select",
"triggers": {
"selection_changed": {
"description": "Triggers after the selected option of one or more dropdowns changes.",
"name": "Selection changed"
}
}
"title": "Select"
}

View File

@@ -1,40 +0,0 @@
"""Provides triggers for selects."""
from homeassistant.components.input_select import DOMAIN as INPUT_SELECT_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
)
from .const import DOMAIN
class SelectionChangedTrigger(EntityTriggerBase):
"""Trigger for select entity when its selection changes."""
_domain_specs = {DOMAIN: DomainSpec(), INPUT_SELECT_DOMAIN: DomainSpec()}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return from_state.state != to_state.state
def is_valid_state(self, state: State) -> bool:
"""Check if the new state is not invalid."""
return state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
TRIGGERS: dict[str, type[Trigger]] = {
"selection_changed": SelectionChangedTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for selects."""
return TRIGGERS

View File

@@ -1,5 +0,0 @@
selection_changed:
target:
entity:
- domain: select
- domain: input_select

View File

@@ -208,15 +208,6 @@ CAPABILITY_TO_SENSORS: dict[
supported_states_attributes=Attribute.SUPPORTED_COOKTOP_OPERATING_STATE,
)
},
Capability.SAMSUNG_CE_MICROFIBER_FILTER_STATUS: {
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
key=Attribute.STATUS,
translation_key="microfiber_filter_blockage",
is_on_key="blockage",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
)
},
}

View File

@@ -73,9 +73,6 @@
"keep_fresh_mode_active": {
"name": "Keep fresh mode active"
},
"microfiber_filter_blockage": {
"name": "Filter blockage"
},
"oven_cavity_status": {
"name": "Second cavity status"
},

View File

@@ -288,8 +288,6 @@ class SongpalEntity(MediaPlayerEntity):
self._volume_min = volume.minVolume
self._volume = volume.volume
self._volume_control = volume
if self._volume_max:
self._attr_volume_step = 1 / self._volume_max
self._attr_is_volume_muted = self._volume_control.is_muted
status = await self._dev.get_power()
@@ -383,6 +381,14 @@ class SongpalEntity(MediaPlayerEntity):
_LOGGER.debug("Setting volume to %s", volume)
return await self._volume_control.set_volume(volume)
async def async_volume_up(self) -> None:
"""Set volume up."""
return await self._volume_control.set_volume(self._volume + 1)
async def async_volume_down(self) -> None:
"""Set volume down."""
return await self._volume_control.set_volume(self._volume - 1)
async def async_turn_on(self) -> None:
"""Turn the device on."""
try:

View File

@@ -8,7 +8,6 @@ from http import HTTPStatus
import logging
from xml.parsers.expat import ExpatError
from aiohttp import ClientSession
import voluptuous as vol
import xmltodict
@@ -151,7 +150,7 @@ async def async_setup_platform(
apikey = config[CONF_API_KEY]
bandwidthcap = config[CONF_TOTAL_BANDWIDTH]
ts_data = StartcaData(websession, apikey, bandwidthcap)
ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap)
ret = await ts_data.async_update()
if ret is False:
_LOGGER.error("Invalid Start.ca API key: %s", apikey)
@@ -177,9 +176,7 @@ async def async_setup_platform(
class StartcaSensor(SensorEntity):
"""Representation of Start.ca Bandwidth sensor."""
def __init__(
self, startcadata: StartcaData, name: str, description: SensorEntityDescription
) -> None:
def __init__(self, startcadata, name, description: SensorEntityDescription) -> None:
"""Initialize the sensor."""
self.entity_description = description
self.startcadata = startcadata
@@ -197,10 +194,9 @@ class StartcaSensor(SensorEntity):
class StartcaData:
"""Get data from Start.ca API."""
def __init__(
self, websession: ClientSession, api_key: str, bandwidth_cap: int
) -> None:
def __init__(self, loop, websession, api_key, bandwidth_cap):
"""Initialize the data object."""
self.loop = loop
self.websession = websession
self.api_key = api_key
self.bandwidth_cap = bandwidth_cap
@@ -219,7 +215,7 @@ class StartcaData:
return float(value) * 10**-9
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> bool:
async def async_update(self):
"""Get the Start.ca bandwidth data from the web service."""
_LOGGER.debug("Updating Start.ca usage data")
url = f"https://www.start.ca/support/usage/api?key={self.api_key}"

View File

@@ -66,7 +66,6 @@ class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
discovery_info = self._discovered_devices_info[self._discovered_device]
title = discovery_info.name
errors: dict[str, str] = {}
if user_input is not None:
# see if we can create a device with the access token
device = VictronBluetoothDeviceData(user_input[CONF_ACCESS_TOKEN])
@@ -77,13 +76,12 @@ class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
title=title,
data=user_input,
)
errors["base"] = "invalid_access_token"
return self.async_abort(reason="invalid_access_token")
return self.async_show_form(
step_id="access_token",
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
description_placeholders={"title": title},
errors=errors,
)
async def async_step_user(

View File

@@ -8,11 +8,9 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_access_token": "Invalid encryption key for instant readout",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
},
"error": {
"invalid_access_token": "Invalid encryption key for instant readout"
},
"flow_title": "{title}",
"step": {
"access_token": {

View File

@@ -1,29 +0,0 @@
"""Provides conditions for windows."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_WINDOW: dict[str, str] = {
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.WINDOW,
COVER_DOMAIN: CoverDeviceClass.WINDOW,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_WINDOW),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_WINDOW),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for windows."""
return CONDITIONS

View File

@@ -1,28 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: window
- domain: cover
device_class: window
is_open:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: window
- domain: cover
device_class: window

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:window-closed"
},
"is_open": {
"condition": "mdi:window-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:window-closed"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted windows.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted windows to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more windows are closed.",
"fields": {
"behavior": {
"description": "[%key:component::window::common::condition_behavior_description%]",
"name": "[%key:component::window::common::condition_behavior_name%]"
}
},
"name": "Window is closed"
},
"is_open": {
"description": "Tests if one or more windows are open.",
"fields": {
"behavior": {
"description": "[%key:component::window::common::condition_behavior_description%]",
"name": "[%key:component::window::common::condition_behavior_name%]"
}
},
"name": "Window is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -55,7 +55,6 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.SELECT_SOURCE
)
_attr_volume_step = 1 / MAX_VOL
def __init__(
self,
@@ -148,6 +147,20 @@ class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity
await self.hass.async_add_executor_job(self._set_volume, int(volume * MAX_VOL))
self._async_update_attrs_write_ha_state()
async def async_volume_up(self) -> None:
"""Volume up the media player."""
await self.hass.async_add_executor_job(
self._set_volume, min(self._status.volume + 1, MAX_VOL)
)
self._async_update_attrs_write_ha_state()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.hass.async_add_executor_job(
self._set_volume, max(self._status.volume - 1, 0)
)
self._async_update_attrs_write_ha_state()
def _set_volume(self, volume: int) -> None:
"""Set the volume of the media player."""
# Can't set a new volume level when this zone is muted.

View File

@@ -3,6 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
from zinvolt.models import BatteryState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
@@ -12,25 +14,15 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ZinvoltConfigEntry, ZinvoltData, ZinvoltDeviceCoordinator
from .coordinator import ZinvoltConfigEntry, ZinvoltDeviceCoordinator
from .entity import ZinvoltEntity
POINT_ENTITIES = {
"communication": BinarySensorDeviceClass.PROBLEM,
"voltage": BinarySensorDeviceClass.PROBLEM,
"current": BinarySensorDeviceClass.PROBLEM,
"temperature": BinarySensorDeviceClass.HEAT,
"charge": BinarySensorDeviceClass.PROBLEM,
"discharge": BinarySensorDeviceClass.PROBLEM,
"other": BinarySensorDeviceClass.PROBLEM,
}
@dataclass(kw_only=True, frozen=True)
class ZinvoltBatteryStateDescription(BinarySensorEntityDescription):
"""Binary sensor description for Zinvolt battery state."""
is_on_fn: Callable[[ZinvoltData], bool]
is_on_fn: Callable[[BatteryState], bool]
SENSORS: tuple[ZinvoltBatteryStateDescription, ...] = (
@@ -39,7 +31,7 @@ SENSORS: tuple[ZinvoltBatteryStateDescription, ...] = (
translation_key="on_grid",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda state: state.battery.current_power.on_grid,
is_on_fn=lambda state: state.current_power.on_grid,
),
)
@@ -51,18 +43,11 @@ async def async_setup_entry(
) -> None:
"""Initialize the entries."""
entities: list[BinarySensorEntity] = [
async_add_entities(
ZinvoltBatteryStateBinarySensor(coordinator, description)
for description in SENSORS
for coordinator in entry.runtime_data.values()
]
entities.extend(
ZinvoltPointBinarySensor(coordinator, point)
for coordinator in entry.runtime_data.values()
for point in coordinator.data.points
if point in POINT_ENTITIES
)
async_add_entities(entities)
class ZinvoltBatteryStateBinarySensor(ZinvoltEntity, BinarySensorEntity):
@@ -78,35 +63,9 @@ class ZinvoltBatteryStateBinarySensor(ZinvoltEntity, BinarySensorEntity):
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.data.battery.serial_number}.{description.key}"
)
self._attr_unique_id = f"{coordinator.data.serial_number}.{description.key}"
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return self.entity_description.is_on_fn(self.coordinator.data)
class ZinvoltPointBinarySensor(ZinvoltEntity, BinarySensorEntity):
"""Zinvolt battery state binary sensor."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, coordinator: ZinvoltDeviceCoordinator, point: str) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.point = point
self._attr_translation_key = point
self._attr_device_class = POINT_ENTITIES[point]
self._attr_unique_id = f"{coordinator.data.battery.serial_number}.{point}"
@property
def available(self) -> bool:
"""Return the availability of the binary sensor."""
return super().available and self.point in self.coordinator.data.points
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return not self.coordinator.data.points[self.point]

View File

@@ -1,6 +1,5 @@
"""Coordinator for Zinvolt."""
from dataclasses import dataclass
from datetime import timedelta
import logging
@@ -19,17 +18,7 @@ _LOGGER = logging.getLogger(__name__)
type ZinvoltConfigEntry = ConfigEntry[dict[str, ZinvoltDeviceCoordinator]]
@dataclass
class ZinvoltData:
"""Data for the Zinvolt integration."""
battery: BatteryState
sw_version: str
model: str
points: dict[str, bool]
class ZinvoltDeviceCoordinator(DataUpdateCoordinator[ZinvoltData]):
class ZinvoltDeviceCoordinator(DataUpdateCoordinator[BatteryState]):
"""Class for Zinvolt devices."""
def __init__(
@@ -50,23 +39,12 @@ class ZinvoltDeviceCoordinator(DataUpdateCoordinator[ZinvoltData]):
self.battery = battery
self.client = client
async def _async_update_data(self) -> ZinvoltData:
async def _async_update_data(self) -> BatteryState:
"""Update data from Zinvolt."""
try:
battery_state = await self.client.get_battery_status(
self.battery.identifier
)
battery_unit = await self.client.get_battery_unit(
self.battery.identifier, self.battery.serial_number
)
return await self.client.get_battery_status(self.battery.identifier)
except ZinvoltError as err:
raise UpdateFailed(
translation_key="update_failed",
translation_domain=DOMAIN,
) from err
return ZinvoltData(
battery_state,
battery_unit.version.current_version,
battery_unit.battery_model,
{point.point.lower(): point.normal for point in battery_unit.points},
)

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.core import HomeAssistant
@@ -17,7 +16,7 @@ async def async_get_config_entry_diagnostics(
return {
"coordinators": [
{
coordinator.battery.identifier: asdict(coordinator.data),
coordinator.battery.identifier: coordinator.data.to_dict(),
}
for coordinator in entry.runtime_data.values()
],

View File

@@ -16,10 +16,8 @@ class ZinvoltEntity(CoordinatorEntity[ZinvoltDeviceCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.data.battery.serial_number)},
identifiers={(DOMAIN, coordinator.data.serial_number)},
manufacturer="Zinvolt",
name=coordinator.battery.name,
serial_number=coordinator.data.battery.serial_number,
model_id=coordinator.data.model,
sw_version=coordinator.data.sw_version,
serial_number=coordinator.data.serial_number,
)

View File

@@ -4,6 +4,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from zinvolt import ZinvoltClient
from zinvolt.models import BatteryState
from homeassistant.components.number import (
NumberDeviceClass,
@@ -14,7 +15,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower, UnitOfT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ZinvoltConfigEntry, ZinvoltData, ZinvoltDeviceCoordinator
from .coordinator import ZinvoltConfigEntry, ZinvoltDeviceCoordinator
from .entity import ZinvoltEntity
@@ -22,8 +23,8 @@ from .entity import ZinvoltEntity
class ZinvoltBatteryStateDescription(NumberEntityDescription):
"""Number description for Zinvolt battery state."""
max_fn: Callable[[ZinvoltData], int] | None = None
value_fn: Callable[[ZinvoltData], int]
max_fn: Callable[[BatteryState], int] | None = None
value_fn: Callable[[BatteryState], int]
set_value_fn: Callable[[ZinvoltClient, str, int], Awaitable[None]]
@@ -34,19 +35,19 @@ NUMBERS: tuple[ZinvoltBatteryStateDescription, ...] = (
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda state: state.battery.global_settings.max_output,
value_fn=lambda state: state.global_settings.max_output,
set_value_fn=lambda client, battery_id, value: client.set_max_output(
battery_id, value
),
native_min_value=0,
max_fn=lambda state: state.battery.global_settings.max_output_limit,
max_fn=lambda state: state.global_settings.max_output_limit,
),
ZinvoltBatteryStateDescription(
key="upper_threshold",
translation_key="upper_threshold",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda state: state.battery.global_settings.battery_upper_threshold,
value_fn=lambda state: state.global_settings.battery_upper_threshold,
set_value_fn=lambda client, battery_id, value: client.set_upper_threshold(
battery_id, value
),
@@ -58,7 +59,7 @@ NUMBERS: tuple[ZinvoltBatteryStateDescription, ...] = (
translation_key="lower_threshold",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda state: state.battery.global_settings.battery_lower_threshold,
value_fn=lambda state: state.global_settings.battery_lower_threshold,
set_value_fn=lambda client, battery_id, value: client.set_lower_threshold(
battery_id, value
),
@@ -71,7 +72,7 @@ NUMBERS: tuple[ZinvoltBatteryStateDescription, ...] = (
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=NumberDeviceClass.DURATION,
value_fn=lambda state: state.battery.global_settings.standby_time,
value_fn=lambda state: state.global_settings.standby_time,
set_value_fn=lambda client, battery_id, value: client.set_standby_time(
battery_id, value
),
@@ -108,9 +109,7 @@ class ZinvoltBatteryStateNumber(ZinvoltEntity, NumberEntity):
"""Initialize the number."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.data.battery.serial_number}.{description.key}"
)
self._attr_unique_id = f"{coordinator.data.serial_number}.{description.key}"
@property
def native_max_value(self) -> float:

View File

@@ -3,6 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
from zinvolt.models import BatteryState
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -13,7 +15,7 @@ from homeassistant.const import PERCENTAGE, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ZinvoltConfigEntry, ZinvoltData, ZinvoltDeviceCoordinator
from .coordinator import ZinvoltConfigEntry, ZinvoltDeviceCoordinator
from .entity import ZinvoltEntity
@@ -21,7 +23,7 @@ from .entity import ZinvoltEntity
class ZinvoltBatteryStateDescription(SensorEntityDescription):
"""Sensor description for Zinvolt battery state."""
value_fn: Callable[[ZinvoltData], float]
value_fn: Callable[[BatteryState], float]
SENSORS: tuple[ZinvoltBatteryStateDescription, ...] = (
@@ -30,14 +32,14 @@ SENSORS: tuple[ZinvoltBatteryStateDescription, ...] = (
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda state: state.battery.current_power.state_of_charge,
value_fn=lambda state: state.current_power.state_of_charge,
),
ZinvoltBatteryStateDescription(
key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda state: 0 - state.battery.current_power.power_socket_output,
value_fn=lambda state: 0 - state.current_power.power_socket_output,
),
)
@@ -69,9 +71,7 @@ class ZinvoltBatteryStateSensor(ZinvoltEntity, SensorEntity):
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.data.battery.serial_number}.{description.key}"
)
self._attr_unique_id = f"{coordinator.data.serial_number}.{description.key}"
@property
def native_value(self) -> float:

View File

@@ -26,26 +26,8 @@
},
"entity": {
"binary_sensor": {
"charge": {
"name": "Charge"
},
"communication": {
"name": "Communication"
},
"current": {
"name": "Current"
},
"discharge": {
"name": "Discharge"
},
"on_grid": {
"name": "Grid connection"
},
"other": {
"name": "Other problems"
},
"voltage": {
"name": "Voltage"
}
},
"number": {

View File

@@ -8,6 +8,7 @@ import collections.abc
from collections.abc import Callable, Generator, Iterable
from copy import deepcopy
from datetime import datetime, timedelta
from enum import Enum
from functools import cache, lru_cache, partial, wraps
import json
import logging
@@ -57,7 +58,10 @@ from homeassistant.core import (
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, location as loc_helper
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.translation import async_translate_state
from homeassistant.helpers.translation import (
async_translate_state,
async_translate_state_attr,
)
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import convert, location as location_util
from homeassistant.util.async_ import run_callback_threadsafe
@@ -807,6 +811,48 @@ class StateTranslated:
return "<template StateTranslated>"
class StateAttrTranslated:
"""Class to represent a translated state attribute value in a template."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
def __call__(self, entity_id: str, attribute: str) -> Any:
"""Retrieve translated state attribute value if available."""
state = _get_state_if_valid(self._hass, entity_id)
if state is None:
return None
attr_value = state.attributes.get(attribute)
if attr_value is None:
return None
if not isinstance(attr_value, str | Enum):
return attr_value
domain = state.domain
device_class = state.attributes.get("device_class")
entry = er.async_get(self._hass).async_get(entity_id)
platform = None if entry is None else entry.platform
translation_key = None if entry is None else entry.translation_key
return async_translate_state_attr(
self._hass,
str(attr_value),
domain,
platform,
translation_key,
device_class,
attribute,
)
def __repr__(self) -> str:
"""Representation of Translated state attribute."""
return "<template StateAttrTranslated>"
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
@@ -1989,6 +2035,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"is_state_attr",
"is_state",
"state_attr",
"state_attr_translated",
"state_translated",
"states",
]
@@ -2036,9 +2083,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["is_state_attr"] = hassfunction(is_state_attr)
self.globals["is_state"] = hassfunction(is_state)
self.globals["state_attr"] = hassfunction(state_attr)
self.globals["state_attr_translated"] = StateAttrTranslated(hass)
self.globals["state_translated"] = StateTranslated(hass)
self.globals["states"] = AllStates(hass)
self.filters["state_attr"] = self.globals["state_attr"]
self.filters["state_attr_translated"] = self.globals["state_attr_translated"]
self.filters["state_translated"] = self.globals["state_translated"]
self.filters["states"] = self.globals["states"]
self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context)
@@ -2047,7 +2096,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
def is_safe_callable(self, obj):
"""Test if callback is safe."""
return isinstance(
obj, (AllStates, StateTranslated)
obj, (AllStates, StateAttrTranslated, StateTranslated)
) or super().is_safe_callable(obj)
def is_safe_attribute(self, obj, attr, value):

View File

@@ -492,3 +492,43 @@ def async_translate_state(
return translations[localize_key]
return state
@callback
def async_translate_state_attr(
hass: HomeAssistant,
attr_value: str,
domain: str,
platform: str | None,
translation_key: str | None,
device_class: str | None,
attribute_name: str,
) -> str:
"""Translate provided state attribute value using cached translations for currently selected language."""
language = hass.config.language
if platform is not None and translation_key is not None:
localize_key = (
f"component.{platform}.entity.{domain}"
f".{translation_key}.state_attributes.{attribute_name}"
f".state.{attr_value}"
)
translations = async_get_cached_translations(hass, language, "entity")
if localize_key in translations:
return translations[localize_key]
translations = async_get_cached_translations(hass, language, "entity_component")
if device_class is not None:
localize_key = (
f"component.{domain}.entity_component.{device_class}"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
localize_key = (
f"component.{domain}.entity_component._"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
return attr_value

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

2
requirements_all.txt generated
View File

@@ -593,7 +593,7 @@ avea==1.6.1
# avion==0.10
# homeassistant.components.axis
axis==67
axis==66
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7

View File

@@ -545,7 +545,7 @@ autoskope_client==1.4.1
av==16.0.1
# homeassistant.components.axis
axis==67
axis==66
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7

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

@@ -138,27 +138,6 @@ async def test_clear_zone_override(
mock_fcn.assert_awaited_once_with()
@pytest.mark.parametrize("install", ["default"])
async def test_clear_zone_override_legacy(
hass: HomeAssistant,
zone_id: str,
) -> None:
"""Test Evohome's clear_zone_override service with the legacy entity_id."""
# EvoZoneMode.FOLLOW_SCHEDULE
with patch("evohomeasync2.zone.Zone.reset") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.CLEAR_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
},
blocking=True,
)
mock_fcn.assert_awaited_once_with()
@pytest.mark.parametrize("install", ["default"])
async def test_set_zone_override(
hass: HomeAssistant,
@@ -201,48 +180,6 @@ async def test_set_zone_override(
)
@pytest.mark.parametrize("install", ["default"])
async def test_set_zone_override_legacy(
hass: HomeAssistant,
zone_id: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test Evohome's set_zone_override service with the legacy entity_id."""
freezer.move_to("2024-07-10T12:00:00+00:00")
# EvoZoneMode.PERMANENT_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(19.5, until=None)
# EvoZoneMode.TEMPORARY_OVERRIDE
with patch("evohomeasync2.zone.Zone.set_temperature") as mock_fcn:
await hass.services.async_call(
DOMAIN,
EvoService.SET_ZONE_OVERRIDE,
{
ATTR_ENTITY_ID: zone_id,
ATTR_SETPOINT: 19.5,
ATTR_DURATION: {"minutes": 135},
},
blocking=True,
)
mock_fcn.assert_awaited_once_with(
19.5, until=datetime(2024, 7, 10, 14, 15, tzinfo=UTC)
)
@pytest.mark.parametrize("install", ["default"])
@pytest.mark.parametrize(
("service", "service_data"),

View File

@@ -1,367 +0,0 @@
"""Test garage_door conditions."""
from typing import Any
import pytest
from homeassistant.components.cover import ATTR_IS_CLOSED, CoverState
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
target_entities,
)
@pytest.fixture
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple binary sensor entities associated with different targets."""
return await target_entities(hass, "binary_sensor")
@pytest.fixture
async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple cover entities associated with different targets."""
return await target_entities(hass, "cover")
@pytest.mark.parametrize(
"condition",
[
"garage_door.is_closed",
"garage_door.is_open",
],
)
async def test_garage_door_conditions_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
) -> None:
"""Test the garage_door conditions are gated by the labs flag."""
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
# --- binary_sensor tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
condition="garage_door.is_open",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
),
*parametrize_condition_states_any(
condition="garage_door.is_closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
),
],
)
async def test_garage_door_binary_sensor_condition_behavior_any(
hass: HomeAssistant,
target_binary_sensors: 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 garage_door condition for binary_sensor with 'any' behavior."""
await assert_condition_behavior_any(
hass,
target_entities=target_binary_sensors,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
condition="garage_door.is_open",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
),
*parametrize_condition_states_all(
condition="garage_door.is_closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
),
],
)
async def test_garage_door_binary_sensor_condition_behavior_all(
hass: HomeAssistant,
target_binary_sensors: 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 garage_door condition for binary_sensor with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_binary_sensors,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- cover tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
condition="garage_door.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
),
*parametrize_condition_states_any(
condition="garage_door.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
),
],
)
async def test_garage_door_cover_condition_behavior_any(
hass: HomeAssistant,
target_covers: 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 garage_door condition for cover entities with 'any' behavior."""
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,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
condition="garage_door.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
),
*parametrize_condition_states_all(
condition="garage_door.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
),
],
)
async def test_garage_door_cover_condition_behavior_all(
hass: HomeAssistant,
target_covers: 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 garage_door condition for cover entities with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- Cross-domain device class exclusion test ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
(
"condition_key",
"binary_sensor_matching",
"binary_sensor_non_matching",
"cover_matching",
"cover_matching_is_closed",
"cover_non_matching",
"cover_non_matching_is_closed",
),
[
(
"garage_door.is_open",
STATE_ON,
STATE_OFF,
CoverState.OPEN,
False,
CoverState.CLOSED,
True,
),
(
"garage_door.is_closed",
STATE_OFF,
STATE_ON,
CoverState.CLOSED,
True,
CoverState.OPEN,
False,
),
],
)
async def test_garage_door_condition_excludes_non_garage_door_device_class(
hass: HomeAssistant,
condition_key: str,
binary_sensor_matching: str,
binary_sensor_non_matching: str,
cover_matching: str,
cover_matching_is_closed: bool,
cover_non_matching: str,
cover_non_matching_is_closed: bool,
) -> None:
"""Test garage_door condition excludes entities without device_class garage_door."""
entity_id_garage_door = "binary_sensor.test_garage_door"
entity_id_window = "binary_sensor.test_window"
entity_id_cover_garage = "cover.test_garage"
entity_id_cover_door = "cover.test_door"
all_entities = [
entity_id_garage_door,
entity_id_window,
entity_id_cover_garage,
entity_id_cover_door,
]
# Set matching states on all entities
hass.states.async_set(
entity_id_garage_door,
binary_sensor_matching,
{ATTR_DEVICE_CLASS: "garage_door"},
)
hass.states.async_set(
entity_id_window, binary_sensor_matching, {ATTR_DEVICE_CLASS: "window"}
)
hass.states.async_set(
entity_id_cover_garage,
cover_matching,
{ATTR_DEVICE_CLASS: "garage", ATTR_IS_CLOSED: cover_matching_is_closed},
)
hass.states.async_set(
entity_id_cover_door,
cover_matching,
{ATTR_DEVICE_CLASS: "door", ATTR_IS_CLOSED: cover_matching_is_closed},
)
await hass.async_block_till_done()
condition_any = await create_target_condition(
hass,
condition=condition_key,
target={CONF_ENTITY_ID: all_entities},
behavior="any",
)
# Matching entities in matching state - condition should be True
assert condition_any(hass) is True
# Set matching entities to non-matching state
hass.states.async_set(
entity_id_garage_door,
binary_sensor_non_matching,
{ATTR_DEVICE_CLASS: "garage_door"},
)
hass.states.async_set(
entity_id_cover_garage,
cover_non_matching,
{ATTR_DEVICE_CLASS: "garage", ATTR_IS_CLOSED: cover_non_matching_is_closed},
)
await hass.async_block_till_done()
# Wrong device class entities still in matching state, but should be excluded
assert condition_any(hass) is False

View File

@@ -1,235 +0,0 @@
"""Test gate conditions."""
from typing import Any
import pytest
from homeassistant.components.cover import ATTR_IS_CLOSED, CoverState
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
target_entities,
)
@pytest.fixture
async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple cover entities associated with different targets."""
return await target_entities(hass, "cover")
@pytest.mark.parametrize(
"condition",
[
"gate.is_closed",
"gate.is_open",
],
)
async def test_gate_conditions_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
) -> None:
"""Test the gate conditions are gated by the labs flag."""
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
condition="gate.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
),
*parametrize_condition_states_any(
condition="gate.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
),
],
)
async def test_gate_cover_condition_behavior_any(
hass: HomeAssistant,
target_covers: 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 gate condition for cover entities with 'any' behavior."""
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,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
condition="gate.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
),
*parametrize_condition_states_all(
condition="gate.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
),
],
)
async def test_gate_cover_condition_behavior_all(
hass: HomeAssistant,
target_covers: 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 gate condition for cover entities with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- Device class exclusion test ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
(
"condition_key",
"cover_matching",
"cover_matching_is_closed",
"cover_non_matching",
"cover_non_matching_is_closed",
),
[
(
"gate.is_open",
CoverState.OPEN,
False,
CoverState.CLOSED,
True,
),
(
"gate.is_closed",
CoverState.CLOSED,
True,
CoverState.OPEN,
False,
),
],
)
async def test_gate_condition_excludes_non_gate_device_class(
hass: HomeAssistant,
condition_key: str,
cover_matching: str,
cover_matching_is_closed: bool,
cover_non_matching: str,
cover_non_matching_is_closed: bool,
) -> None:
"""Test gate condition excludes entities without device_class gate."""
entity_id_gate = "cover.test_gate"
entity_id_door = "cover.test_door"
# Set matching states on all entities
hass.states.async_set(
entity_id_gate,
cover_matching,
{ATTR_DEVICE_CLASS: "gate", ATTR_IS_CLOSED: cover_matching_is_closed},
)
hass.states.async_set(
entity_id_door,
cover_matching,
{ATTR_DEVICE_CLASS: "door", ATTR_IS_CLOSED: cover_matching_is_closed},
)
await hass.async_block_till_done()
condition_any = await create_target_condition(
hass,
condition=condition_key,
target={CONF_ENTITY_ID: [entity_id_gate, entity_id_door]},
behavior="any",
)
# Matching entity in matching state - condition should be True
assert condition_any(hass) is True
# Set matching entity to non-matching state
hass.states.async_set(
entity_id_gate,
cover_non_matching,
{ATTR_DEVICE_CLASS: "gate", ATTR_IS_CLOSED: cover_non_matching_is_closed},
)
await hass.async_block_till_done()
# Wrong device class entity still in matching state, but should be excluded
assert condition_any(hass) is False

File diff suppressed because it is too large Load Diff

View File

@@ -1087,90 +1087,3 @@ async def test_supervisor_issue_deprecated_addon(
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.parametrize(
"all_setup_requests", [{"include_addons": True}], indirect=True
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_deprecated_arch_addon(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue for add-on using deprecated architecture or machine."""
mock_resolution_info(
supervisor_client,
issues=[
Issue(
type=IssueType.DEPRECATED_ARCH_ADDON,
context=ContextType.ADDON,
reference="test",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REMOVE,
context=ContextType.ADDON,
reference="test",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
resp = await client.post(
"/api/repairs/issues/fix",
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"type": "form",
"flow_id": flow_id,
"handler": "hassio",
"step_id": "addon_execute_remove",
"data_schema": [],
"errors": None,
"description_placeholders": {
"reference": "test",
"addon": "test",
"help_url": "https://www.home-assistant.io/help/",
"community_url": "https://community.home-assistant.io/",
},
"last_step": True,
"preview": None,
}
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"type": "create_entry",
"flow_id": flow_id,
"handler": "hassio",
"description": None,
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)

View File

@@ -112,7 +112,7 @@ async def test_node_startall_stopall_buttons(
[
("button.vm_web_start", 100, "start"),
("button.vm_web_stop", 100, "stop"),
("button.vm_web_restart", 100, "reboot"),
("button.vm_web_restart", 100, "restart"),
("button.vm_web_hibernate", 100, "hibernate"),
("button.vm_web_reset", 100, "reset"),
],
@@ -147,7 +147,7 @@ async def test_vm_buttons(
[
("button.ct_nginx_start", 200, "start"),
("button.ct_nginx_stop", 200, "stop"),
("button.ct_nginx_restart", 200, "reboot"),
("button.ct_nginx_restart", 200, "restart"),
],
)
async def test_container_buttons(
@@ -278,7 +278,7 @@ async def test_vm_buttons_exceptions(
(
"button.ct_nginx_restart",
200,
"reboot",
"restart",
ConnectTimeout("timeout"),
),
(

View File

@@ -85,22 +85,6 @@ def patch_get_vehicles(vehicle_type: str) -> Generator[None]:
).vehicleLinks
)
# Mock supports_endpoint to return True for soc-levels (battery SoC),
# but only when vehicleDetails is available.
vehicle_details = return_value.vehicleLinks[0].vehicleDetails
if vehicle_details is not None:
original_supports_endpoint = vehicle_details.supports_endpoint
def mock_supports_endpoint(endpoint: str) -> bool:
if endpoint == "soc-levels":
vehicle_fixtures = MOCK_VEHICLES.get(fixture_code)
return bool(
vehicle_fixtures and "battery_soc" in vehicle_fixtures["endpoints"]
)
return original_supports_endpoint(endpoint)
vehicle_details.supports_endpoint = mock_supports_endpoint
with patch(
"renault_api.renault_account.RenaultAccount.get_vehicles",
return_value=return_value,
@@ -117,11 +101,6 @@ def _get_fixtures(vehicle_type: str) -> MappingProxyType:
if "battery_status" in mock_vehicle["endpoints"]
else load_fixture("renault/no_data.json")
).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema),
"battery_soc": schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture(f"renault/{mock_vehicle['endpoints']['battery_soc']}")
if "battery_soc" in mock_vehicle["endpoints"]
else load_fixture("renault/no_data.json")
).get_attributes(schemas.KamereonVehicleBatterySocDataSchema),
"charge_mode": schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}")
if "charge_mode" in mock_vehicle["endpoints"]
@@ -172,9 +151,6 @@ def patch_get_vehicle_data() -> Generator[dict[str, AsyncMock]]:
patch(
"renault_api.renault_vehicle.RenaultVehicle.get_battery_status"
) as get_battery_status,
patch(
"renault_api.renault_vehicle.RenaultVehicle.get_battery_soc"
) as get_battery_soc,
patch(
"renault_api.renault_vehicle.RenaultVehicle.get_charge_mode"
) as get_charge_mode,
@@ -200,7 +176,6 @@ def patch_get_vehicle_data() -> Generator[dict[str, AsyncMock]]:
):
yield {
"battery_status": get_battery_status,
"battery_soc": get_battery_soc,
"charge_mode": get_charge_mode,
"charging_settings": get_charging_settings,
"cockpit": get_cockpit,

View File

@@ -17,7 +17,6 @@ MOCK_VEHICLES = {
"zoe_40": {
"endpoints": {
"battery_status": "battery_status_charging.json",
"battery_soc": "battery_soc.json",
"charge_mode": "charge_mode_always.json",
"cockpit": "cockpit_ev.json",
"hvac_status": "hvac_status.1.json",
@@ -26,7 +25,6 @@ MOCK_VEHICLES = {
"zoe_50": {
"endpoints": {
"battery_status": "battery_status_not_charging.json",
"battery_soc": "battery_soc.json",
"charge_mode": "charge_mode_schedule.json",
"charging_settings": "charging_settings.json",
"cockpit": "cockpit_ev.json",

View File

@@ -1,9 +0,0 @@
{
"data": {
"type": "BatterySocLevels",
"id": "guid",
"attributes": {
"action": "set"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"data": {
"type": "Car",
"id": "VF1AAAAA555777999",
"attributes": {
"socMin": 15,
"socTarget": 80
}
}
}

View File

@@ -14,10 +14,6 @@
'plugStatus': 1,
'timestamp': '2020-01-12T21:40:16Z',
}),
'battery_soc': dict({
'socMin': 15,
'socTarget': 80,
}),
'charge_mode': dict({
'chargeMode': 'always',
}),
@@ -223,10 +219,6 @@
'plugStatus': 1,
'timestamp': '2020-01-12T21:40:16Z',
}),
'battery_soc': dict({
'socMin': 15,
'socTarget': 80,
}),
'charge_mode': dict({
'chargeMode': 'always',
}),

View File

@@ -1,361 +0,0 @@
# serializer version: 1
# name: test_number_empty[zoe_40][number.reg_zoe_40_minimum_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Minimum charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Minimum charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_min',
'unique_id': 'vf1zoe40vin_charge_limit_min',
'unit_of_measurement': '%',
})
# ---
# name: test_number_empty[zoe_40][number.reg_zoe_40_minimum_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Minimum charge level',
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_number_empty[zoe_40][number.reg_zoe_40_target_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Target charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_target',
'unique_id': 'vf1zoe40vin_charge_limit_target',
'unit_of_measurement': '%',
})
# ---
# name: test_number_empty[zoe_40][number.reg_zoe_40_target_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Target charge level',
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_number_errors[zoe_40][number.reg_zoe_40_minimum_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Minimum charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Minimum charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_min',
'unique_id': 'vf1zoe40vin_charge_limit_min',
'unit_of_measurement': '%',
})
# ---
# name: test_number_errors[zoe_40][number.reg_zoe_40_minimum_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Minimum charge level',
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_number_errors[zoe_40][number.reg_zoe_40_target_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Target charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_target',
'unique_id': 'vf1zoe40vin_charge_limit_target',
'unit_of_measurement': '%',
})
# ---
# name: test_number_errors[zoe_40][number.reg_zoe_40_target_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Target charge level',
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_numbers[zoe_40][number.reg_zoe_40_minimum_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Minimum charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Minimum charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_min',
'unique_id': 'vf1zoe40vin_charge_limit_min',
'unit_of_measurement': '%',
})
# ---
# name: test_numbers[zoe_40][number.reg_zoe_40_minimum_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Minimum charge level',
'max': 45,
'min': 15,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_minimum_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '15',
})
# ---
# name: test_numbers[zoe_40][number.reg_zoe_40_target_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Target charge level',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Target charge level',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge_limit_target',
'unique_id': 'vf1zoe40vin_charge_limit_target',
'unit_of_measurement': '%',
})
# ---
# name: test_numbers[zoe_40][number.reg_zoe_40_target_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'REG-ZOE-40 Target charge level',
'max': 100,
'min': 55,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.reg_zoe_40_target_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---

View File

@@ -1,199 +0,0 @@
"""Tests for Renault number entities."""
from collections.abc import Generator
from typing import Any
from unittest.mock import patch
import pytest
from renault_api.kamereon import schemas
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.components.renault.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from tests.common import async_load_fixture, snapshot_platform
pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles")
@pytest.fixture(autouse=True)
def override_platforms() -> Generator[None]:
"""Override PLATFORMS."""
with patch("homeassistant.components.renault.PLATFORMS", [Platform.NUMBER]):
yield
@pytest.mark.usefixtures("fixtures_with_data")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
async def test_numbers(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test for Renault number entities."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.usefixtures("fixtures_with_no_data")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
async def test_number_empty(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test for Renault number entities with empty data from Renault."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
async def test_number_errors(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test for Renault number entities with temporary failure."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.usefixtures("fixtures_with_access_denied_exception")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
async def test_number_access_denied(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for Renault number entities with access denied failure."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(entity_registry.entities) == 0
@pytest.mark.usefixtures("fixtures_with_not_supported_exception")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
async def test_number_not_supported(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for Renault number entities with not supported failure."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(entity_registry.entities) == 0
@pytest.mark.usefixtures("fixtures_with_data")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
@pytest.mark.parametrize(
("service_data", "expected_min", "expected_target"),
[
(
{
ATTR_ENTITY_ID: "number.reg_zoe_40_minimum_charge_level",
ATTR_VALUE: 20,
},
20,
80,
),
(
{
ATTR_ENTITY_ID: "number.reg_zoe_40_target_charge_level",
ATTR_VALUE: 90,
},
15,
90,
),
],
)
async def test_number_action(
hass: HomeAssistant,
config_entry: ConfigEntry,
service_data: dict[str, Any],
expected_min: int,
expected_target: int,
) -> None:
"""Test that service invokes renault_api with correct data for min charge limit."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"renault_api.renault_vehicle.RenaultVehicle.set_battery_soc",
return_value=(
schemas.KamereonVehicleBatterySocActionDataSchema.loads(
await async_load_fixture(hass, "action.set_battery_soc.json", DOMAIN)
)
),
) as mock_action:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data=service_data,
blocking=True,
)
assert len(mock_action.mock_calls) == 1
mock_action.assert_awaited_once_with(min=expected_min, target=expected_target)
# Verify optimistic update of coordinator data
assert hass.states.get("number.reg_zoe_40_minimum_charge_level").state == str(
expected_min
)
assert hass.states.get("number.reg_zoe_40_target_charge_level").state == str(
expected_target
)
@pytest.mark.usefixtures("fixtures_with_no_data")
@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True)
@pytest.mark.parametrize(
"service_data",
[
{
ATTR_ENTITY_ID: "number.reg_zoe_40_minimum_charge_level",
ATTR_VALUE: 20,
},
{
ATTR_ENTITY_ID: "number.reg_zoe_40_target_charge_level",
ATTR_VALUE: 90,
},
],
)
async def test_number_action_(
hass: HomeAssistant, config_entry: ConfigEntry, service_data: dict[str, Any]
) -> None:
"""Test that service invokes renault_api with correct data for min charge limit."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with pytest.raises(
ServiceValidationError,
match="Battery state of charge data is currently unavailable",
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data=service_data,
blocking=True,
)

View File

@@ -197,9 +197,9 @@ async def test_sensor_throttling_after_init(
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_50", 1, 420), # 7 coordinators => 7 minutes interval
("zoe_50", 1, 360), # 6 coordinators => 6 minutes interval
("captur_fuel", 1, 180), # 3 coordinators => 3 minutes interval
("multi", 2, 540), # 9 coordinators => 9 minutes interval
("multi", 2, 480), # 8 coordinators => 8 minutes interval
],
indirect=["vehicle_type"],
)
@@ -236,9 +236,9 @@ async def test_dynamic_scan_interval(
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_50", 1, 360), # (7-1) coordinators => 6 minutes interval
("zoe_50", 1, 300), # (6-1) coordinators => 5 minutes interval
("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval
("multi", 2, 480), # (10-2) coordinators => 8 minutes interval
("multi", 2, 420), # (9-2) coordinators => 7 minutes interval
],
indirect=["vehicle_type"],
)

View File

@@ -1,219 +0,0 @@
"""Test select trigger."""
import pytest
from homeassistant.const import CONF_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, ServiceCall
from tests.components.common import (
TriggerStateDescription,
arm_trigger,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@pytest.fixture
async def target_selects(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple select entities associated with different targets."""
return await target_entities(hass, "select")
@pytest.fixture
async def target_input_selects(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple input_select entities associated with different targets."""
return await target_entities(hass, "input_select")
@pytest.mark.parametrize("trigger_key", ["select.selection_changed"])
async def test_select_triggers_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
) -> None:
"""Test the select triggers are gated by the labs flag."""
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
STATE_SEQUENCE = [
(
"select.selection_changed",
[
{"included_state": {"state": None, "attributes": {}}, "count": 0},
{"included_state": {"state": "option_a", "attributes": {}}, "count": 0},
{"included_state": {"state": "option_b", "attributes": {}}, "count": 1},
],
),
(
"select.selection_changed",
[
{"included_state": {"state": "option_a", "attributes": {}}, "count": 0},
{"included_state": {"state": "option_b", "attributes": {}}, "count": 1},
{"included_state": {"state": "option_c", "attributes": {}}, "count": 1},
],
),
(
"select.selection_changed",
[
{
"included_state": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
{"included_state": {"state": "option_a", "attributes": {}}, "count": 0},
{"included_state": {"state": "option_b", "attributes": {}}, "count": 1},
{
"included_state": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
],
),
(
"select.selection_changed",
[
{
"included_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{"included_state": {"state": "option_a", "attributes": {}}, "count": 0},
{"included_state": {"state": "option_b", "attributes": {}}, "count": 1},
{
"included_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
],
),
]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("select"),
)
@pytest.mark.parametrize(("trigger", "states"), STATE_SEQUENCE)
async def test_select_state_trigger(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_selects: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
states: list[TriggerStateDescription],
) -> None:
"""Test that the select trigger fires when targeted select state changes."""
await _assert_select_trigger_fires(
hass,
service_calls=service_calls,
target_entities=target_selects,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("input_select"),
)
@pytest.mark.parametrize(("trigger", "states"), STATE_SEQUENCE)
async def test_input_select_state_trigger(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_input_selects: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
states: list[TriggerStateDescription],
) -> None:
"""Test that the select trigger fires when targeted input_select state changes."""
await _assert_select_trigger_fires(
hass,
service_calls=service_calls,
target_entities=target_input_selects,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
states=states,
)
async def _assert_select_trigger_fires(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_entities: dict[str, list[str]],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
states: list[TriggerStateDescription],
) -> None:
"""Test that the select trigger fires when targeted state changes."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
# Set all entities to the initial state
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_state"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, None, trigger_target_config)
for state in states[1:]:
included_state = state["included_state"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# Check if changing other targeted entities also triggers
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 len(service_calls) == (entities_in_target - 1) * state["count"]
service_calls.clear()
# --- Cross-domain test ---
@pytest.mark.usefixtures("enable_labs_preview_features")
async def test_select_trigger_fires_for_both_domains(
hass: HomeAssistant,
service_calls: list[ServiceCall],
) -> None:
"""Test that the select trigger fires for both select and input_select entities."""
entity_id_select = "select.test_select"
entity_id_input_select = "input_select.test_input_select"
hass.states.async_set(entity_id_select, "option_a")
hass.states.async_set(entity_id_input_select, "option_a")
await hass.async_block_till_done()
await arm_trigger(
hass,
"select.selection_changed",
None,
{CONF_ENTITY_ID: [entity_id_select, entity_id_input_select]},
)
# select entity changes - should trigger
hass.states.async_set(entity_id_select, "option_b")
await hass.async_block_till_done()
assert len(service_calls) == 1
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_select
service_calls.clear()
# input_select entity changes - should also trigger
hass.states.async_set(entity_id_input_select, "option_b")
await hass.async_block_till_done()
assert len(service_calls) == 1
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_input_select
service_calls.clear()

View File

@@ -2325,57 +2325,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_blockage-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_blockage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Filter blockage',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Filter blockage',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'microfiber_filter_blockage',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_samsungce.microfiberFilterStatus_status_status',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_blockage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Filtro in microfibra Filter blockage',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_blockage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -209,7 +209,7 @@ async def test_bad_return_code(
status=HTTPStatus.NOT_FOUND,
)
scd = StartcaData(async_get_clientsession(hass), "NOTAKEY", 400)
scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400)
result = await scd.async_update()
assert result is False
@@ -223,7 +223,7 @@ async def test_bad_json_decode(
"https://www.start.ca/support/usage/api?key=NOTAKEY", text="this is not xml"
)
scd = StartcaData(async_get_clientsession(hass), "NOTAKEY", 400)
scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400)
result = await scd.async_update()
assert result is False

View File

@@ -56,36 +56,6 @@ async def test_async_step_bluetooth_valid_device(
assert set(flow_result.data.keys()) == {CONF_ACCESS_TOKEN}
async def test_async_step_bluetooth_invalid_key_retry(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test wrong key via bluetooth discovery shows error and allows retry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_BLUETOOTH},
data=VICTRON_VEBUS_SERVICE_INFO,
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "access_token"
# enter wrong key
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ACCESS_TOKEN: VICTRON_TEST_WRONG_TOKEN},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "access_token"
assert result.get("errors") == {"base": "invalid_access_token"}
# retry with correct key
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ACCESS_TOKEN: VICTRON_VEBUS_TOKEN},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == VICTRON_VEBUS_SERVICE_INFO.name
@pytest.mark.parametrize(
("source", "service_info", "expected_reason"),
[
@@ -124,9 +94,7 @@ async def test_abort_scenarios(
async def test_async_step_user_with_devices_found(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_discovered_service_info: AsyncMock,
hass: HomeAssistant, mock_discovered_service_info: AsyncMock
) -> None:
"""Test setup from service info cache with devices found."""
result = await hass.config_entries.flow.async_init(
@@ -143,21 +111,12 @@ async def test_async_step_user_with_devices_found(
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "access_token"
# test invalid access token shows error and allows retry
# test invalid access token (valid already tested above)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_ACCESS_TOKEN: VICTRON_TEST_WRONG_TOKEN}
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "access_token"
assert result.get("errors") == {"base": "invalid_access_token"}
# test retry with valid access token succeeds
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ACCESS_TOKEN: VICTRON_VEBUS_TOKEN},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == VICTRON_VEBUS_SERVICE_INFO.name
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "invalid_access_token"
async def test_async_step_user_device_added_between_steps(

View File

@@ -1,363 +0,0 @@
"""Test window conditions."""
from typing import Any
import pytest
from homeassistant.components.cover import ATTR_IS_CLOSED, CoverState
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
target_entities,
)
@pytest.fixture
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple binary sensor entities associated with different targets."""
return await target_entities(hass, "binary_sensor")
@pytest.fixture
async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple cover entities associated with different targets."""
return await target_entities(hass, "cover")
@pytest.mark.parametrize(
"condition",
[
"window.is_closed",
"window.is_open",
],
)
async def test_window_conditions_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
) -> None:
"""Test the window conditions are gated by the labs flag."""
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
# --- binary_sensor tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
condition="window.is_open",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
*parametrize_condition_states_any(
condition="window.is_closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
],
)
async def test_window_binary_sensor_condition_behavior_any(
hass: HomeAssistant,
target_binary_sensors: 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 window condition for binary_sensor with 'any' behavior."""
await assert_condition_behavior_any(
hass,
target_entities=target_binary_sensors,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("binary_sensor"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
condition="window.is_open",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
*parametrize_condition_states_all(
condition="window.is_closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
],
)
async def test_window_binary_sensor_condition_behavior_all(
hass: HomeAssistant,
target_binary_sensors: 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 window condition for binary_sensor with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_binary_sensors,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- cover tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
condition="window.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
*parametrize_condition_states_any(
condition="window.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
],
)
async def test_window_cover_condition_behavior_any(
hass: HomeAssistant,
target_covers: 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 window condition for cover entities with 'any' behavior."""
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,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
condition="window.is_open",
target_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
other_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
*parametrize_condition_states_all(
condition="window.is_closed",
target_states=[
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
other_states=[
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
),
],
)
async def test_window_cover_condition_behavior_all(
hass: HomeAssistant,
target_covers: 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 window condition for cover entities with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- Cross-domain device class exclusion test ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
(
"condition_key",
"binary_sensor_matching",
"binary_sensor_non_matching",
"cover_matching",
"cover_matching_is_closed",
"cover_non_matching",
"cover_non_matching_is_closed",
),
[
(
"window.is_open",
STATE_ON,
STATE_OFF,
CoverState.OPEN,
False,
CoverState.CLOSED,
True,
),
(
"window.is_closed",
STATE_OFF,
STATE_ON,
CoverState.CLOSED,
True,
CoverState.OPEN,
False,
),
],
)
async def test_window_condition_excludes_non_window_device_class(
hass: HomeAssistant,
condition_key: str,
binary_sensor_matching: str,
binary_sensor_non_matching: str,
cover_matching: str,
cover_matching_is_closed: bool,
cover_non_matching: str,
cover_non_matching_is_closed: bool,
) -> None:
"""Test window condition excludes entities without device_class window."""
entity_id_window = "binary_sensor.test_window"
entity_id_door = "binary_sensor.test_door"
entity_id_cover_window = "cover.test_window"
entity_id_cover_garage = "cover.test_garage"
all_entities = [
entity_id_window,
entity_id_door,
entity_id_cover_window,
entity_id_cover_garage,
]
# Set matching states on all entities
hass.states.async_set(
entity_id_window, binary_sensor_matching, {ATTR_DEVICE_CLASS: "window"}
)
hass.states.async_set(
entity_id_door, binary_sensor_matching, {ATTR_DEVICE_CLASS: "door"}
)
hass.states.async_set(
entity_id_cover_window,
cover_matching,
{ATTR_DEVICE_CLASS: "window", ATTR_IS_CLOSED: cover_matching_is_closed},
)
hass.states.async_set(
entity_id_cover_garage,
cover_matching,
{ATTR_DEVICE_CLASS: "garage", ATTR_IS_CLOSED: cover_matching_is_closed},
)
await hass.async_block_till_done()
condition_any = await create_target_condition(
hass,
condition=condition_key,
target={CONF_ENTITY_ID: all_entities},
behavior="any",
)
# Matching entities in matching state - condition should be True
assert condition_any(hass) is True
# Set matching entities to non-matching state
hass.states.async_set(
entity_id_window, binary_sensor_non_matching, {ATTR_DEVICE_CLASS: "window"}
)
hass.states.async_set(
entity_id_cover_window,
cover_non_matching,
{ATTR_DEVICE_CLASS: "window", ATTR_IS_CLOSED: cover_non_matching_is_closed},
)
await hass.async_block_till_done()
# Wrong device class entities still in matching state, but should be excluded
assert condition_any(hass) is False

View File

@@ -4,7 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from zinvolt.models import BatteryListResponse, BatteryState, BatteryUnit
from zinvolt.models import BatteryListResponse, BatteryState
from homeassistant.components.zinvolt.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
@@ -54,7 +54,4 @@ def mock_zinvolt_client() -> Generator[AsyncMock]:
client.get_battery_status.return_value = BatteryState.from_json(
load_fixture("current_state.json", DOMAIN)
)
client.get_battery_unit.return_value = BatteryUnit.from_json(
load_fixture("battery_unit.json", DOMAIN)
)
yield client

View File

@@ -1,62 +0,0 @@
{
"usn": "ZVS25011000009148",
"type": "BATTERY",
"version": {
"currentVersion": "V1.02-V0.00.000",
"status": "NO_UPDATE"
},
"points": [
{
"point": "COMMUNICATION",
"unitType": "BATTERY",
"message": "Communication",
"pieces": [],
"normal": true
},
{
"point": "VOLTAGE",
"unitType": "BATTERY",
"message": "Voltage",
"pieces": [],
"normal": true
},
{
"point": "CURRENT",
"unitType": "BATTERY",
"message": "Current",
"pieces": [],
"normal": true
},
{
"point": "TEMPERATURE",
"unitType": "BATTERY",
"message": "Temperature",
"pieces": [],
"normal": true
},
{
"point": "CHARGE",
"unitType": "BATTERY",
"message": "Charge",
"pieces": [],
"normal": true
},
{
"point": "DISCHARGE",
"unitType": "BATTERY",
"message": "Discharge",
"pieces": [],
"normal": true
},
{
"point": "OTHER",
"unitType": "BATTERY",
"message": "Other",
"pieces": [],
"normal": true
}
],
"batteryModel": "ZVS4000",
"power": -6,
"temperature": 0
}

View File

@@ -1,208 +1,4 @@
# serializer version: 1
# name: test_all_entities[binary_sensor.zinvolt_batterij_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charge',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Charge',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge',
'unique_id': 'ZVG011025120088.charge',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_charge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Charge',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_communication-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_communication',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Communication',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Communication',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'communication',
'unique_id': 'ZVG011025120088.communication',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_communication-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Communication',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_communication',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_current-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Current',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'current',
'unique_id': 'ZVG011025120088.current',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Current',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_discharge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_discharge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Discharge',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Discharge',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'discharge',
'unique_id': 'ZVG011025120088.discharge',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_discharge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Discharge',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_discharge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_grid_connection-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -254,156 +50,3 @@
'state': 'on',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_heat-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_heat',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Heat',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.HEAT: 'heat'>,
'original_icon': None,
'original_name': 'Heat',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': 'ZVG011025120088.temperature',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_heat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'heat',
'friendly_name': 'Zinvolt Batterij Heat',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_heat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_other_problems-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_other_problems',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Other problems',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Other problems',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'other',
'unique_id': 'ZVG011025120088.other',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_other_problems-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Other problems',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_other_problems',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_voltage-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.zinvolt_batterij_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Voltage',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Voltage',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'voltage',
'unique_id': 'ZVG011025120088.voltage',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_voltage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Zinvolt Batterij Voltage',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.zinvolt_batterij_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

Some files were not shown because too many files have changed in this diff Show More