Merge branch 'dev' of github.com:home-assistant/core into zha_3phase_current_entities

This commit is contained in:
abmantis 2024-12-24 15:36:29 +00:00
commit 109a6a428d
105 changed files with 2332 additions and 224 deletions

View File

@ -40,7 +40,7 @@ env:
CACHE_VERSION: 11 CACHE_VERSION: 11
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.1" HA_SHORT_VERSION: "2025.2"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']" ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version

View File

@ -1103,8 +1103,8 @@ build.json @home-assistant/supervisor
/tests/components/otbr/ @home-assistant/core /tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund /homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund /tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14 /homeassistant/components/overkiz/ @imicknl
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14 /tests/components/overkiz/ @imicknl
/homeassistant/components/ovo_energy/ @timmo001 /homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001 /tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas /homeassistant/components/p1_monitor/ @klaasnicolaas
@ -1135,8 +1135,8 @@ build.json @home-assistant/supervisor
/tests/components/plaato/ @JohNan /tests/components/plaato/ @JohNan
/homeassistant/components/plex/ @jjlawren /homeassistant/components/plex/ @jjlawren
/tests/components/plex/ @jjlawren /tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck /homeassistant/components/plugwise/ @CoMPaTech @bouwew
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck /tests/components/plugwise/ @CoMPaTech @bouwew
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa /homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
/tests/components/plum_lightpad/ @ColinHarrington @prystupa /tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike /homeassistant/components/point/ @fredrike
@ -1573,8 +1573,8 @@ build.json @home-assistant/supervisor
/tests/components/triggercmd/ @rvmey /tests/components/triggercmd/ @rvmey
/homeassistant/components/tts/ @home-assistant/core /homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core /tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck /homeassistant/components/tuya/ @Tuya @zlinoliver
/tests/components/tuya/ @Tuya @zlinoliver @frenck /tests/components/tuya/ @Tuya @zlinoliver
/homeassistant/components/twentemilieu/ @frenck /homeassistant/components/twentemilieu/ @frenck
/tests/components/twentemilieu/ @frenck /tests/components/twentemilieu/ @frenck
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen /homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen

View File

@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
from .const import CONF_STATION_UPDATES, DOMAIN, PLATFORMS from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DOMAIN, PLATFORMS
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
latitude = entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE] longitude = entry.data[CONF_LONGITUDE]
update_features: int = UpdateFeature.FORECAST update_features: int = UpdateFeature.FORECAST
if entry.options.get(CONF_RADAR_UPDATES, False):
update_features |= UpdateFeature.RADAR
if entry.options.get(CONF_STATION_UPDATES, True): if entry.options.get(CONF_STATION_UPDATES, True):
update_features |= UpdateFeature.STATION update_features |= UpdateFeature.STATION

View File

@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaOptionsFlowHandler, SchemaOptionsFlowHandler,
) )
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_RADAR_UPDATES, default=False): bool,
vol.Required(CONF_STATION_UPDATES, default=True): bool, vol.Required(CONF_STATION_UPDATES, default=True): bool,
} }
) )

View File

@ -51,8 +51,9 @@ from homeassistant.components.weather import (
from homeassistant.const import Platform from homeassistant.const import Platform
ATTRIBUTION = "Powered by AEMET OpenData" ATTRIBUTION = "Powered by AEMET OpenData"
CONF_RADAR_UPDATES = "radar_updates"
CONF_STATION_UPDATES = "station_updates" CONF_STATION_UPDATES = "station_updates"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
DEFAULT_NAME = "AEMET" DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from aemet_opendata.const import AOD_COORDS from aemet_opendata.const import AOD_COORDS, AOD_IMG_BYTES
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import ( from homeassistant.const import (
@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
TO_REDACT_COORD = [ TO_REDACT_COORD = [
AOD_COORDS, AOD_COORDS,
AOD_IMG_BYTES,
] ]

View File

@ -0,0 +1,86 @@
"""Support for the AEMET OpenData images."""
from __future__ import annotations
from typing import Final
from aemet_opendata.const import AOD_DATETIME, AOD_IMG_BYTES, AOD_IMG_TYPE, AOD_RADAR
from aemet_opendata.helpers import dict_nested_value
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
from .entity import AemetEntity
AEMET_IMAGES: Final[tuple[ImageEntityDescription, ...]] = (
ImageEntityDescription(
key=AOD_RADAR,
translation_key="weather_radar",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AemetConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AEMET OpenData image entities based on a config entry."""
domain_data = config_entry.runtime_data
name = domain_data.name
coordinator = domain_data.coordinator
unique_id = config_entry.unique_id
assert unique_id is not None
async_add_entities(
AemetImage(
hass,
name,
coordinator,
description,
unique_id,
)
for description in AEMET_IMAGES
if dict_nested_value(coordinator.data["lib"], [description.key]) is not None
)
class AemetImage(AemetEntity, ImageEntity):
"""Implementation of an AEMET OpenData image."""
entity_description: ImageEntityDescription
def __init__(
self,
hass: HomeAssistant,
name: str,
coordinator: WeatherUpdateCoordinator,
description: ImageEntityDescription,
unique_id: str,
) -> None:
"""Initialize the image."""
super().__init__(coordinator, name, unique_id)
ImageEntity.__init__(self, hass)
self.entity_description = description
self._attr_unique_id = f"{unique_id}-{description.key}"
self._async_update_attrs()
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Update image attributes."""
image_data = self.get_aemet_value([self.entity_description.key])
self._cached_image = Image(
content_type=image_data.get(AOD_IMG_TYPE),
content=image_data.get(AOD_IMG_BYTES),
)
self._attr_image_last_updated = image_data.get(AOD_DATETIME)

View File

@ -18,10 +18,18 @@
} }
} }
}, },
"entity": {
"image": {
"weather_radar": {
"name": "Weather radar"
}
}
},
"options": { "options": {
"step": { "step": {
"init": { "init": {
"data": { "data": {
"radar_updates": "Gather data from AEMET weather radar",
"station_updates": "Gather data from AEMET weather stations" "station_updates": "Gather data from AEMET weather stations"
} }
} }

View File

@ -6,5 +6,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["apprise"], "loggers": ["apprise"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["apprise==1.9.0"] "requirements": ["apprise==1.9.1"]
} }

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from aiohttp import ClientConnectorError
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
from homeassistant.const import UnitOfPower from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -45,7 +47,13 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Set the state with the value fetched from the inverter.""" """Set the state with the value fetched from the inverter."""
self._attr_native_value = await self._api.get_max_power() try:
status = await self._api.get_max_power()
except (TimeoutError, ClientConnectorError):
self._attr_available = False
else:
self._attr_available = True
self._attr_native_value = status
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Set the desired output power.""" """Set the desired output power."""

View File

@ -5,6 +5,10 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
# Pre-import backup to avoid it being imported
# later when the import executor is busy and delaying
# startup
from . import backup # noqa: F401
from .agent import ( from .agent import (
BackupAgent, BackupAgent,
BackupAgentError, BackupAgentError,

View File

@ -36,7 +36,14 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.signal_type import SignalType from homeassistant.util.signal_type import SignalType
from . import account_link, http_api # Pre-import backup to avoid it being imported
# later when the import executor is busy and delaying
# startup
from . import (
account_link,
backup, # noqa: F401
http_api,
)
from .client import CloudClient from .client import CloudClient
from .const import ( from .const import (
CONF_ACCOUNT_LINK_SERVER, CONF_ACCOUNT_LINK_SERVER,

View File

@ -2,7 +2,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from deebot_client.capabilities import CapabilityExecute, CapabilityLifeSpan from deebot_client.capabilities import (
CapabilityExecute,
CapabilityExecuteTypes,
CapabilityLifeSpan,
)
from deebot_client.commands import StationAction
from deebot_client.events import LifeSpan from deebot_client.events import LifeSpan
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
@ -11,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import EcovacsConfigEntry from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
from .entity import ( from .entity import (
EcovacsCapabilityEntityDescription, EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity, EcovacsDescriptionEntity,
@ -35,6 +40,13 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription):
component: LifeSpan component: LifeSpan
@dataclass(kw_only=True, frozen=True)
class EcovacsStationActionButtonEntityDescription(ButtonEntityDescription):
"""Ecovacs station action button entity description."""
action: StationAction
ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = ( ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = (
EcovacsButtonEntityDescription( EcovacsButtonEntityDescription(
capability_fn=lambda caps: caps.map.relocation if caps.map else None, capability_fn=lambda caps: caps.map.relocation if caps.map else None,
@ -44,6 +56,16 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = (
), ),
) )
STATION_ENTITY_DESCRIPTIONS = tuple(
EcovacsStationActionButtonEntityDescription(
action=action,
key=f"station_action_{action.name.lower()}",
translation_key=f"station_action_{action.name.lower()}",
)
for action in SUPPORTED_STATION_ACTIONS
)
LIFESPAN_ENTITY_DESCRIPTIONS = tuple( LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
EcovacsLifespanButtonEntityDescription( EcovacsLifespanButtonEntityDescription(
component=component, component=component,
@ -74,6 +96,15 @@ async def async_setup_entry(
for description in LIFESPAN_ENTITY_DESCRIPTIONS for description in LIFESPAN_ENTITY_DESCRIPTIONS
if description.component in device.capabilities.life_span.types if description.component in device.capabilities.life_span.types
) )
entities.extend(
EcovacsStationActionButtonEntity(
device, device.capabilities.station.action, description
)
for device in controller.devices
if device.capabilities.station
for description in STATION_ENTITY_DESCRIPTIONS
if description.action in device.capabilities.station.action.types
)
async_add_entities(entities) async_add_entities(entities)
@ -103,3 +134,18 @@ class EcovacsResetLifespanButtonEntity(
await self._device.execute_command( await self._device.execute_command(
self._capability.reset(self.entity_description.component) self._capability.reset(self.entity_description.component)
) )
class EcovacsStationActionButtonEntity(
EcovacsDescriptionEntity[CapabilityExecuteTypes[StationAction]],
ButtonEntity,
):
"""Ecovacs station action button entity."""
entity_description: EcovacsStationActionButtonEntityDescription
async def async_press(self) -> None:
"""Press the button."""
await self._device.execute_command(
self._capability.execute(self.entity_description.action)
)

View File

@ -2,6 +2,7 @@
from enum import StrEnum from enum import StrEnum
from deebot_client.commands import StationAction
from deebot_client.events import LifeSpan from deebot_client.events import LifeSpan
DOMAIN = "ecovacs" DOMAIN = "ecovacs"
@ -19,8 +20,11 @@ SUPPORTED_LIFESPANS = (
LifeSpan.SIDE_BRUSH, LifeSpan.SIDE_BRUSH,
LifeSpan.UNIT_CARE, LifeSpan.UNIT_CARE,
LifeSpan.ROUND_MOP, LifeSpan.ROUND_MOP,
LifeSpan.STATION_FILTER,
) )
SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,)
LEGACY_SUPPORTED_LIFESPANS = ( LEGACY_SUPPORTED_LIFESPANS = (
"main_brush", "main_brush",
"side_brush", "side_brush",

View File

@ -27,11 +27,17 @@
"reset_lifespan_side_brush": { "reset_lifespan_side_brush": {
"default": "mdi:broom" "default": "mdi:broom"
}, },
"reset_lifespan_station_filter": {
"default": "mdi:air-filter"
},
"reset_lifespan_unit_care": { "reset_lifespan_unit_care": {
"default": "mdi:robot-vacuum" "default": "mdi:robot-vacuum"
}, },
"reset_lifespan_round_mop": { "reset_lifespan_round_mop": {
"default": "mdi:broom" "default": "mdi:broom"
},
"station_action_empty_dustbin": {
"default": "mdi:delete-restore"
} }
}, },
"event": { "event": {
@ -72,6 +78,9 @@
"lifespan_side_brush": { "lifespan_side_brush": {
"default": "mdi:broom" "default": "mdi:broom"
}, },
"lifespan_station_filter": {
"default": "mdi:air-filter"
},
"lifespan_unit_care": { "lifespan_unit_care": {
"default": "mdi:robot-vacuum" "default": "mdi:robot-vacuum"
}, },
@ -87,6 +96,9 @@
"network_ssid": { "network_ssid": {
"default": "mdi:wifi" "default": "mdi:wifi"
}, },
"station_state": {
"default": "mdi:home"
},
"stats_area": { "stats_area": {
"default": "mdi:floor-plan" "default": "mdi:floor-plan"
}, },

View File

@ -16,6 +16,7 @@ from deebot_client.events import (
NetworkInfoEvent, NetworkInfoEvent,
StatsEvent, StatsEvent,
TotalStatsEvent, TotalStatsEvent,
station,
) )
from sucks import VacBot from sucks import VacBot
@ -46,7 +47,7 @@ from .entity import (
EcovacsLegacyEntity, EcovacsLegacyEntity,
EventT, EventT,
) )
from .util import get_supported_entitites from .util import get_name_key, get_options, get_supported_entitites
@dataclass(kw_only=True, frozen=True) @dataclass(kw_only=True, frozen=True)
@ -136,6 +137,15 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
# Station
EcovacsSensorEntityDescription[station.StationEvent](
capability_fn=lambda caps: caps.station.state if caps.station else None,
value_fn=lambda e: get_name_key(e.state),
key="station_state",
translation_key="station_state",
device_class=SensorDeviceClass.ENUM,
options=get_options(station.State),
),
) )

View File

@ -46,6 +46,9 @@
"relocate": { "relocate": {
"name": "Relocate" "name": "Relocate"
}, },
"reset_lifespan_base_station_filter": {
"name": "Reset station filter lifespan"
},
"reset_lifespan_blade": { "reset_lifespan_blade": {
"name": "Reset blade lifespan" "name": "Reset blade lifespan"
}, },
@ -66,6 +69,9 @@
}, },
"reset_lifespan_side_brush": { "reset_lifespan_side_brush": {
"name": "Reset side brush lifespan" "name": "Reset side brush lifespan"
},
"station_action_empty_dustbin": {
"name": "Empty dustbin"
} }
}, },
"event": { "event": {
@ -107,6 +113,9 @@
} }
} }
}, },
"lifespan_base_station_filter": {
"name": "Station filter lifespan"
},
"lifespan_blade": { "lifespan_blade": {
"name": "Blade lifespan" "name": "Blade lifespan"
}, },
@ -140,6 +149,13 @@
"network_ssid": { "network_ssid": {
"name": "Wi-Fi SSID" "name": "Wi-Fi SSID"
}, },
"station_state": {
"name": "Station state",
"state": {
"idle": "[%key:common::state::idle%]",
"emptying_dustbin": "Emptying dustbin"
}
},
"stats_area": { "stats_area": {
"name": "Area cleaned" "name": "Area cleaned"
}, },

View File

@ -7,6 +7,8 @@ import random
import string import string
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from deebot_client.events.station import State
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify from homeassistant.util import slugify
@ -47,4 +49,13 @@ def get_supported_entitites(
@callback @callback
def get_name_key(enum: Enum) -> str: def get_name_key(enum: Enum) -> str:
"""Return the lower case name of the enum.""" """Return the lower case name of the enum."""
if enum is State.EMPTYING:
# Will be fixed in the next major release of deebot-client
return "emptying_dustbin"
return enum.name.lower() return enum.name.lower()
@callback
def get_options(enum: type[Enum]) -> list[str]:
"""Return the options for the enum."""
return [get_name_key(option) for option in enum]

View File

@ -13,7 +13,7 @@ rules:
docs-actions: done docs-actions: done
docs-high-level-description: done docs-high-level-description: done
docs-installation-instructions: done docs-installation-instructions: done
docs-removal-instructions: todo docs-removal-instructions: done
entity-event-setup: entity-event-setup:
status: exempt status: exempt
comment: > comment: >

View File

@ -27,6 +27,7 @@ class FritzboxCoordinatorData:
devices: dict[str, FritzhomeDevice] devices: dict[str, FritzhomeDevice]
templates: dict[str, FritzhomeTemplate] templates: dict[str, FritzhomeTemplate]
supported_color_properties: dict[str, tuple[dict, list]]
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]): class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
@ -49,7 +50,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.new_devices: set[str] = set() self.new_devices: set[str] = set()
self.new_templates: set[str] = set() self.new_templates: set[str] = set()
self.data = FritzboxCoordinatorData({}, {}) self.data = FritzboxCoordinatorData({}, {}, {})
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the coordinator.""" """Set up the coordinator."""
@ -120,6 +121,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
devices = self.fritz.get_devices() devices = self.fritz.get_devices()
device_data = {} device_data = {}
supported_color_properties = self.data.supported_color_properties
for device in devices: for device in devices:
# assume device as unavailable, see #55799 # assume device as unavailable, see #55799
if ( if (
@ -136,6 +138,13 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
device_data[device.ain] = device device_data[device.ain] = device
# pre-load supported colors and color temps for new devices
if device.has_color and device.ain not in supported_color_properties:
supported_color_properties[device.ain] = (
device.get_colors(),
device.get_color_temps(),
)
template_data = {} template_data = {}
if self.has_templates: if self.has_templates:
templates = self.fritz.get_templates() templates = self.fritz.get_templates()
@ -145,7 +154,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.new_devices = device_data.keys() - self.data.devices.keys() self.new_devices = device_data.keys() - self.data.devices.keys()
self.new_templates = template_data.keys() - self.data.templates.keys() self.new_templates = template_data.keys() - self.data.templates.keys()
return FritzboxCoordinatorData(devices=device_data, templates=template_data) return FritzboxCoordinatorData(
devices=device_data,
templates=template_data,
supported_color_properties=supported_color_properties,
)
async def _async_update_data(self) -> FritzboxCoordinatorData: async def _async_update_data(self) -> FritzboxCoordinatorData:
"""Fetch all device data.""" """Fetch all device data."""

View File

@ -57,7 +57,6 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
) -> None: ) -> None:
"""Initialize the FritzboxLight entity.""" """Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None) super().__init__(coordinator, ain, None)
self._supported_hs: dict[int, list[int]] = {}
self._attr_supported_color_modes = {ColorMode.ONOFF} self._attr_supported_color_modes = {ColorMode.ONOFF}
if self.data.has_color: if self.data.has_color:
@ -65,6 +64,26 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
elif self.data.has_level: elif self.data.has_level:
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
(supported_colors, supported_color_temps) = (
coordinator.data.supported_color_properties.get(self.data.ain, ({}, []))
)
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
self._supported_hs: dict[int, list[int]] = {}
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""If the light is currently on or off.""" """If the light is currently on or off."""
@ -148,30 +167,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off.""" """Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off) await self.hass.async_add_executor_job(self.data.set_state_off)
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
async def async_added_to_hass(self) -> None:
"""Get light attributes from device after entity is added to hass."""
await super().async_added_to_hass()
def _get_color_data() -> tuple[dict, list]:
return (self.data.get_colors(), self.data.get_color_temps())
(
supported_colors,
supported_color_temps,
) = await self.hass.async_add_executor_job(_get_color_data)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]

View File

@ -11,5 +11,6 @@
"documentation": "https://www.home-assistant.io/integrations/fronius", "documentation": "https://www.home-assistant.io/integrations/fronius",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyfronius"], "loggers": ["pyfronius"],
"quality_scale": "gold",
"requirements": ["PyFronius==0.7.3"] "requirements": ["PyFronius==0.7.3"]
} }

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241127.8"] "requirements": ["home-assistant-frontend==20241223.1"]
} }

View File

@ -77,7 +77,7 @@
}, },
"error": { "error": {
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"unknown_with_details": "[%key:common::config_flow::error::unknown_with_details]", "unknown_with_details": "[%key:component::generic::config::error::unknown_with_details%]",
"already_exists": "[%key:component::generic::config::error::already_exists%]", "already_exists": "[%key:component::generic::config::error::already_exists%]",
"unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]",
"unable_still_load_auth": "[%key:component::generic::config::error::unable_still_load_auth%]", "unable_still_load_auth": "[%key:component::generic::config::error::unable_still_load_auth%]",

View File

@ -0,0 +1 @@
"""Virtual integration: Harvey."""

View File

@ -0,0 +1,6 @@
{
"domain": "harvey",
"name": "Harvey",
"integration_type": "virtual",
"supported_by": "aquacell"
}

View File

@ -118,9 +118,7 @@ class HistoryStats:
<= current_period_end_timestamp <= current_period_end_timestamp
): ):
self._history_current_period.append( self._history_current_period.append(
HistoryState( HistoryState(new_state.state, new_state.last_changed_timestamp)
new_state.state, new_state.last_changed.timestamp()
)
) )
new_data = True new_data = True
if not new_data and current_period_end_timestamp < now_timestamp: if not new_data and current_period_end_timestamp < now_timestamp:
@ -131,6 +129,16 @@ class HistoryStats:
await self._async_history_from_db( await self._async_history_from_db(
current_period_start_timestamp, current_period_end_timestamp current_period_start_timestamp, current_period_end_timestamp
) )
if event and (new_state := event.data["new_state"]) is not None:
if (
current_period_start_timestamp
<= floored_timestamp(new_state.last_changed)
<= current_period_end_timestamp
):
self._history_current_period.append(
HistoryState(new_state.state, new_state.last_changed_timestamp)
)
self._previous_run_before_start = False self._previous_run_before_start = False
seconds_matched, match_count = self._async_compute_seconds_and_changes( seconds_matched, match_count = self._async_compute_seconds_and_changes(

View File

@ -113,12 +113,17 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.sensor.getSensor(self.device) self.device = await self.hive.sensor.getSensor(self.device)
self.attributes = self.device.get("attributes", {}) self.attributes = self.device.get("attributes", {})
self._attr_is_on = self.device["status"]["state"]
if self.device["hiveType"] != "Connectivity": if self.device["hiveType"] != "Connectivity":
self._attr_available = self.device["deviceData"].get("online") self._attr_available = (
self.device["deviceData"].get("online") and "status" in self.device
)
else: else:
self._attr_available = True self._attr_available = True
if self._attr_available:
self._attr_is_on = self.device["status"].get("state")
class HiveSensorEntity(HiveEntity, BinarySensorEntity): class HiveSensorEntity(HiveEntity, BinarySensorEntity):
"""Hive Sensor Entity.""" """Hive Sensor Entity."""

View File

@ -12,5 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/idasen_desk", "documentation": "https://www.home-assistant.io/integrations/idasen_desk",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["idasen-ha==2.6.3"] "requirements": ["idasen-ha==2.6.3"]
} }

View File

@ -17,9 +17,9 @@ rules:
status: exempt status: exempt
comment: | comment: |
This integration does not provide additional actions. This integration does not provide additional actions.
docs-high-level-description: todo docs-high-level-description: done
docs-installation-instructions: done docs-installation-instructions: done
docs-removal-instructions: todo docs-removal-instructions: done
entity-event-setup: done entity-event-setup: done
entity-unique-id: done entity-unique-id: done
has-entity-name: done has-entity-name: done

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["keba_kecontact"], "loggers": ["keba_kecontact"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["keba-kecontact==1.1.0"] "requirements": ["keba-kecontact==1.3.0"]
} }

View File

@ -46,9 +46,13 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@ -451,18 +455,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Modbus component.""" """Set up Modbus component."""
if DOMAIN not in config: if DOMAIN not in config:
return True return True
async def _reload_config(call: Event | ServiceCall) -> None:
"""Reload Modbus."""
if DOMAIN not in hass.data:
_LOGGER.error("Modbus cannot reload, because it was never loaded")
return
hubs = hass.data[DOMAIN]
for name in hubs:
await hubs[name].async_close()
reset_platforms = async_get_platforms(hass, DOMAIN)
for reset_platform in reset_platforms:
_LOGGER.debug("Reload modbus resetting platform: %s", reset_platform.domain)
await reset_platform.async_reset()
reload_config = await async_integration_yaml_config(hass, DOMAIN)
if not reload_config:
_LOGGER.debug("Modbus not present anymore")
return
_LOGGER.debug("Modbus reloading")
await async_modbus_setup(hass, reload_config)
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
return await async_modbus_setup( return await async_modbus_setup(
hass, hass,
config, config,
) )
async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None:
"""Release modbus resources."""
if DOMAIN not in hass.data:
_LOGGER.error("Modbus cannot reload, because it was never loaded")
return
_LOGGER.debug("Modbus reloading")
hubs = hass.data[DOMAIN]
for name in hubs:
await hubs[name].async_close()

View File

@ -34,7 +34,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@ -125,8 +124,6 @@ async def async_modbus_setup(
) -> bool: ) -> bool:
"""Set up Modbus component.""" """Set up Modbus component."""
await async_setup_reload_service(hass, DOMAIN, [DOMAIN])
if config[DOMAIN]: if config[DOMAIN]:
config[DOMAIN] = check_config(hass, config[DOMAIN]) config[DOMAIN] = check_config(hass, config[DOMAIN])
if not config[DOMAIN]: if not config[DOMAIN]:

View File

@ -13,7 +13,7 @@ from homeassistant.helpers import entity_registry as er
from .const import _LOGGER from .const import _LOGGER
PLATFORMS: list[Platform] = [Platform.LIGHT] PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
type NikoHomeControlConfigEntry = ConfigEntry[NHCController] type NikoHomeControlConfigEntry = ConfigEntry[NHCController]

View File

@ -0,0 +1,54 @@
"""Cover Platform for Niko Home Control."""
from __future__ import annotations
from typing import Any
from nhc.cover import NHCCover
from homeassistant.components.cover import CoverEntity, CoverEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import NikoHomeControlConfigEntry
from .entity import NikoHomeControlEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: NikoHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Niko Home Control cover entry."""
controller = entry.runtime_data
async_add_entities(
NikoHomeControlCover(cover, controller, entry.entry_id)
for cover in controller.covers
)
class NikoHomeControlCover(NikoHomeControlEntity, CoverEntity):
"""Representation of a Niko Cover."""
_attr_name = None
_attr_supported_features: CoverEntityFeature = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)
_action: NHCCover
def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._action.open()
def close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._action.close()
def stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._action.stop()
def update_state(self):
"""Update HA state."""
self._attr_is_closed = self._action.state == 0

View File

@ -1,14 +1,7 @@
{ {
"domain": "overkiz", "domain": "overkiz",
"name": "Overkiz", "name": "Overkiz",
"codeowners": [ "codeowners": ["@imicknl"],
"@imicknl",
"@vlebourl",
"@tetienne",
"@nyroDev",
"@tronix117",
"@alexfp14"
],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [
{ {

View File

@ -16,6 +16,7 @@ from peblar import (
PeblarEVInterface, PeblarEVInterface,
PeblarMeter, PeblarMeter,
PeblarSystem, PeblarSystem,
PeblarSystemInformation,
PeblarUserConfiguration, PeblarUserConfiguration,
PeblarVersions, PeblarVersions,
) )
@ -24,7 +25,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from tests.components.peblar.conftest import PeblarSystemInformation
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER

View File

@ -20,7 +20,7 @@
"data_description": { "data_description": {
"password": "[%key:component::peblar::config::step::user::data_description::password%]" "password": "[%key:component::peblar::config::step::user::data_description::password%]"
}, },
"description": "Reauthenticate with your Peblar RV charger.\n\nTo do so, you will need to enter your new password you use to log into Peblar's device web interface." "description": "Reauthenticate with your Peblar EV charger.\n\nTo do so, you will need to enter your new password you use to log into Peblar EV charger' web interface."
}, },
"reconfigure": { "reconfigure": {
"data": { "data": {
@ -31,7 +31,7 @@
"host": "[%key:component::peblar::config::step::user::data_description::host%]", "host": "[%key:component::peblar::config::step::user::data_description::host%]",
"password": "[%key:component::peblar::config::step::user::data_description::password%]" "password": "[%key:component::peblar::config::step::user::data_description::password%]"
}, },
"description": "Reconfigure your Peblar EV charger.\n\nThis allows you to change the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface." "description": "Reconfigure your Peblar EV charger.\n\nThis allows you to change the IP address of your Peblar EV charger and the password you use to log into its web interface."
}, },
"user": { "user": {
"data": { "data": {
@ -39,10 +39,10 @@
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
}, },
"data_description": { "data_description": {
"host": "The hostname or IP address of your Peblar charger on your home network.", "host": "The hostname or IP address of your Peblar EV charger on your home network.",
"password": "The same password as you use to log in to the Peblar device' local web interface." "password": "The same password as you use to log in to the Peblar EV charger' local web interface."
}, },
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant." "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar EV charger and the password you use to log into its web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
}, },
"zeroconf_confirm": { "zeroconf_confirm": {
"data": { "data": {
@ -51,7 +51,7 @@
"data_description": { "data_description": {
"password": "[%key:component::peblar::config::step::user::data_description::password%]" "password": "[%key:component::peblar::config::step::user::data_description::password%]"
}, },
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant." "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar EV charger' web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
} }
} }
}, },
@ -164,13 +164,13 @@
}, },
"exceptions": { "exceptions": {
"authentication_error": { "authentication_error": {
"message": "An authentication failure occurred while communicating with the Peblar device." "message": "An authentication failure occurred while communicating with the Peblar EV charger."
}, },
"communication_error": { "communication_error": {
"message": "An error occurred while communicating with the Peblar device: {error}" "message": "An error occurred while communicating with the Peblar EV charger: {error}"
}, },
"unknown_error": { "unknown_error": {
"message": "An unknown error occurred while communicating with the Peblar device: {error}" "message": "An unknown error occurred while communicating with the Peblar EV charger: {error}"
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"domain": "plugwise", "domain": "plugwise",
"name": "Plugwise", "name": "Plugwise",
"codeowners": ["@CoMPaTech", "@bouwew", "@frenck"], "codeowners": ["@CoMPaTech", "@bouwew"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise",
"integration_type": "hub", "integration_type": "hub",

View File

@ -28,7 +28,14 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.event_type import EventType from homeassistant.util.event_type import EventType
from . import entity_registry, websocket_api # Pre-import backup to avoid it being imported
# later when the import executor is busy and delaying
# startup
from . import (
backup, # noqa: F401
entity_registry,
websocket_api,
)
from .const import ( # noqa: F401 from .const import ( # noqa: F401
CONF_DB_INTEGRITY_CHECK, CONF_DB_INTEGRITY_CHECK,
DOMAIN, DOMAIN,

View File

@ -115,7 +115,7 @@
}, },
"firmware_update": { "firmware_update": {
"title": "Reolink firmware update required", "title": "Reolink firmware update required",
"description": "\"{name}\" with model \"{model}\" and hardware version \"{hw_version}\" is running a old firmware version \"{current_firmware}\", while at least firmware version \"{required_firmware}\" is required for proper operation of the Reolink integration. The latest firmware can be downloaded from the [Reolink download center]({download_link})." "description": "\"{name}\" with model \"{model}\" and hardware version \"{hw_version}\" is running a old firmware version \"{current_firmware}\", while at least firmware version \"{required_firmware}\" is required for proper operation of the Reolink integration. The firmware can be updated by pressing \"install\" in the more info dialog of the update entity of \"{name}\" from within Home Assistant. Alternatively, the latest firmware can be downloaded from the [Reolink download center]({download_link})."
}, },
"hdr_switch_deprecated": { "hdr_switch_deprecated": {
"title": "Reolink HDR switch deprecated", "title": "Reolink HDR switch deprecated",

View File

@ -93,7 +93,7 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
) )
self._configured_heat_modes.append(HEAT_MODE.HEATER) self._configured_heat_modes.append(HEAT_MODE.HEATER)
self._attr_preset_modes = [ self._attr_preset_modes = [
HEAT_MODE(mode_num).title for mode_num in self._configured_heat_modes HEAT_MODE(mode_num).name.lower() for mode_num in self._configured_heat_modes
] ]
self._attr_min_temp = self.entity_data[ATTR.MIN_SETPOINT] self._attr_min_temp = self.entity_data[ATTR.MIN_SETPOINT]
@ -137,8 +137,8 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
def preset_mode(self) -> str: def preset_mode(self) -> str:
"""Return current/last preset mode.""" """Return current/last preset mode."""
if self.hvac_mode == HVACMode.OFF: if self.hvac_mode == HVACMode.OFF:
return HEAT_MODE(self._last_preset).title return HEAT_MODE(self._last_preset).name.lower()
return HEAT_MODE(self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE]).title return HEAT_MODE(self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE]).name.lower()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Change the setpoint of the heater.""" """Change the setpoint of the heater."""

View File

@ -189,8 +189,8 @@ SUPPORTED_INTELLICHEM_SENSORS = [
data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS),
key=VALUE.ORP_DOSING_STATE, key=VALUE.ORP_DOSING_STATE,
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=["Dosing", "Mixing", "Monitoring"], options=["dosing", "mixing", "monitoring"],
value_mod=lambda val: DOSE_STATE(val).title, value_mod=lambda val: DOSE_STATE(val).name.lower(),
translation_key="chem_dose_state", translation_key="chem_dose_state",
translation_placeholders={"chem": "ORP"}, translation_placeholders={"chem": "ORP"},
), ),
@ -217,8 +217,8 @@ SUPPORTED_INTELLICHEM_SENSORS = [
data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS), data_root=(DEVICE.INTELLICHEM, GROUP.DOSE_STATUS),
key=VALUE.PH_DOSING_STATE, key=VALUE.PH_DOSING_STATE,
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=["Dosing", "Mixing", "Monitoring"], options=["dosing", "mixing", "monitoring"],
value_mod=lambda val: DOSE_STATE(val).title, value_mod=lambda val: DOSE_STATE(val).name.lower(),
translation_key="chem_dose_state", translation_key="chem_dose_state",
translation_placeholders={"chem": "pH"}, translation_placeholders={"chem": "pH"},
), ),

View File

@ -3,8 +3,9 @@
"service_config_entry_name": "Config entry", "service_config_entry_name": "Config entry",
"service_config_entry_description": "The config entry to use for this action.", "service_config_entry_description": "The config entry to use for this action.",
"climate_preset_solar": "Solar", "climate_preset_solar": "Solar",
"climate_preset_solar_prefered": "Solar Prefered", "climate_preset_solar_preferred": "Solar Preferred",
"climate_preset_heater": "Heater" "climate_preset_heater": "Heater",
"climate_preset_dont_change": "Don't Change"
}, },
"config": { "config": {
"flow_title": "{name}", "flow_title": "{name}",
@ -133,10 +134,30 @@
}, },
"climate": { "climate": {
"body_0": { "body_0": {
"name": "Pool heat" "name": "Pool heat",
"state_attributes": {
"preset_mode": {
"state": {
"solar": "[%key:component::screenlogic::common::climate_preset_solar%]",
"solar_preferred": "[%key:component::screenlogic::common::climate_preset_solar_preferred%]",
"heater": "[%key:component::screenlogic::common::climate_preset_heater%]",
"dont_change": "[%key:component::screenlogic::common::climate_preset_dont_change%]"
}
}
}
}, },
"body_1": { "body_1": {
"name": "Spa heat" "name": "Spa heat",
"state_attributes": {
"preset_mode": {
"state": {
"solar": "[%key:component::screenlogic::common::climate_preset_solar%]",
"solar_preferred": "[%key:component::screenlogic::common::climate_preset_solar_preferred%]",
"heater": "[%key:component::screenlogic::common::climate_preset_heater%]",
"dont_change": "[%key:component::screenlogic::common::climate_preset_dont_change%]"
}
}
}
} }
}, },
"number": { "number": {
@ -191,7 +212,12 @@
"name": "[%key:component::screenlogic::entity::number::salt_tds_ppm::name%]" "name": "[%key:component::screenlogic::entity::number::salt_tds_ppm::name%]"
}, },
"chem_dose_state": { "chem_dose_state": {
"name": "{chem} dosing state" "name": "{chem} dosing state",
"state": {
"dosing": "Dosing",
"mixing": "Mixing",
"monitoring": "Monitoring"
}
}, },
"chem_last_dose_time": { "chem_last_dose_time": {
"name": "{chem} last dose time" "name": "{chem} last dose time"

View File

@ -49,7 +49,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Subaru from a config entry.""" """Set up Subaru from a config entry."""
config = entry.data config = entry.data
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_create_clientsession(hass)
try: try:
controller = SubaruAPI( controller = SubaruAPI(
websession, websession,

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import ENERGY_HISTORY_FIELDS, LOGGER from .const import ENERGY_HISTORY_FIELDS, LOGGER
from .helpers import flatten from .helpers import flatten
VEHICLE_INTERVAL = timedelta(seconds=30) VEHICLE_INTERVAL = timedelta(seconds=60)
VEHICLE_WAIT = timedelta(minutes=15) VEHICLE_WAIT = timedelta(minutes=15)
ENERGY_LIVE_INTERVAL = timedelta(seconds=30) ENERGY_LIVE_INTERVAL = timedelta(seconds=30)
ENERGY_INFO_INTERVAL = timedelta(seconds=30) ENERGY_INFO_INTERVAL = timedelta(seconds=30)

View File

@ -96,6 +96,7 @@ class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntit
entity_description: TPLinkBinarySensorEntityDescription entity_description: TPLinkBinarySensorEntityDescription
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_is_on = cast(bool | None, self._feature.value) self._attr_is_on = cast(bool | None, self._feature.value)
return True

View File

@ -52,15 +52,19 @@ BUTTON_DESCRIPTIONS: Final = [
), ),
TPLinkButtonEntityDescription( TPLinkButtonEntityDescription(
key="pan_left", key="pan_left",
available_fn=lambda dev: dev.is_on,
), ),
TPLinkButtonEntityDescription( TPLinkButtonEntityDescription(
key="pan_right", key="pan_right",
available_fn=lambda dev: dev.is_on,
), ),
TPLinkButtonEntityDescription( TPLinkButtonEntityDescription(
key="tilt_up", key="tilt_up",
available_fn=lambda dev: dev.is_on,
), ),
TPLinkButtonEntityDescription( TPLinkButtonEntityDescription(
key="tilt_down", key="tilt_down",
available_fn=lambda dev: dev.is_on,
), ),
] ]
@ -100,5 +104,6 @@ class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity):
"""Execute action.""" """Execute action."""
await self._feature.set_value(True) await self._feature.set_value(True)
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""No need to update anything.""" """No need to update anything."""
return self.entity_description.available_fn(self._device)

View File

@ -7,7 +7,7 @@ import time
from aiohttp import web from aiohttp import web
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
from kasa import Credentials, Device, Module from kasa import Credentials, Device, Module, StreamResolution
from kasa.smartcam.modules import Camera as CameraModule from kasa.smartcam.modules import Camera as CameraModule
from homeassistant.components import ffmpeg, stream from homeassistant.components import ffmpeg, stream
@ -40,6 +40,7 @@ CAMERA_DESCRIPTIONS: tuple[TPLinkCameraEntityDescription, ...] = (
TPLinkCameraEntityDescription( TPLinkCameraEntityDescription(
key="live_view", key="live_view",
translation_key="live_view", translation_key="live_view",
available_fn=lambda dev: dev.is_on,
), ),
) )
@ -95,7 +96,9 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
"""Initialize a TPlink camera.""" """Initialize a TPlink camera."""
self.entity_description = description self.entity_description = description
self._camera_module = camera_module self._camera_module = camera_module
self._video_url = camera_module.stream_rtsp_url(camera_credentials) self._video_url = camera_module.stream_rtsp_url(
camera_credentials, stream_resolution=StreamResolution.SD
)
self._image: bytes | None = None self._image: bytes | None = None
super().__init__(device, coordinator, parent=parent) super().__init__(device, coordinator, parent=parent)
Camera.__init__(self) Camera.__init__(self)
@ -108,16 +111,19 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
def _get_unique_id(self) -> str: def _get_unique_id(self) -> str:
"""Return unique ID for the entity.""" """Return unique ID for the entity."""
return f"{legacy_device_id(self._device)}-{self.entity_description}" return f"{legacy_device_id(self._device)}-{self.entity_description.key}"
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_is_on = self._camera_module.is_on self._attr_is_on = self._camera_module.is_on
return self.entity_description.available_fn(self._device)
async def stream_source(self) -> str | None: async def stream_source(self) -> str | None:
"""Return the source of the stream.""" """Return the source of the stream."""
return self._video_url return self._camera_module.stream_rtsp_url(
self._camera_credentials, stream_resolution=StreamResolution.HD
)
async def _async_check_stream_auth(self, video_url: str) -> None: async def _async_check_stream_auth(self, video_url: str) -> None:
"""Check for an auth error and start reauth flow.""" """Check for an auth error and start reauth flow."""
@ -148,7 +154,7 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
return self._image return self._image
# Don't try to capture a new image if a stream is running # Don't try to capture a new image if a stream is running
if (self.stream and self.stream.available) or self._http_mpeg_stream_running: if self._http_mpeg_stream_running:
return self._image return self._image
if self._can_stream and (video_url := self._video_url): if self._can_stream and (video_url := self._video_url):

View File

@ -113,7 +113,7 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
await self._state_feature.set_value(False) await self._state_feature.set_value(False)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_current_temperature = cast(float | None, self._temp_feature.value) self._attr_current_temperature = cast(float | None, self._temp_feature.value)
self._attr_target_temperature = cast(float | None, self._target_feature.value) self._attr_target_temperature = cast(float | None, self._target_feature.value)
@ -131,11 +131,12 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
self._mode_feature.value, self._mode_feature.value,
) )
self._attr_hvac_action = HVACAction.OFF self._attr_hvac_action = HVACAction.OFF
return return True
self._attr_hvac_action = STATE_TO_ACTION[ self._attr_hvac_action = STATE_TO_ACTION[
cast(ThermostatState, self._mode_feature.value) cast(ThermostatState, self._mode_feature.value)
] ]
return True
def _get_unique_id(self) -> str: def _get_unique_id(self) -> str:
"""Return unique id.""" """Return unique id."""

View File

@ -89,6 +89,7 @@ class TPLinkFeatureEntityDescription(EntityDescription):
"""Base class for a TPLink feature based entity description.""" """Base class for a TPLink feature based entity description."""
deprecated_info: DeprecatedInfo | None = None deprecated_info: DeprecatedInfo | None = None
available_fn: Callable[[Device], bool] = lambda _: True
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -96,6 +97,7 @@ class TPLinkModuleEntityDescription(EntityDescription):
"""Base class for a TPLink module based entity description.""" """Base class for a TPLink module based entity description."""
deprecated_info: DeprecatedInfo | None = None deprecated_info: DeprecatedInfo | None = None
available_fn: Callable[[Device], bool] = lambda _: True
def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
@ -207,15 +209,18 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB
@abstractmethod @abstractmethod
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Platforms implement this to update the entity internals.""" """Platforms implement this to update the entity internals.
The return value is used to the set the entity available attribute.
"""
raise NotImplementedError raise NotImplementedError
@callback @callback
def _async_call_update_attrs(self) -> None: def _async_call_update_attrs(self) -> None:
"""Call update_attrs and make entity unavailable on errors.""" """Call update_attrs and make entity unavailable on errors."""
try: try:
self._async_update_attrs() available = self._async_update_attrs()
except Exception as ex: # noqa: BLE001 except Exception as ex: # noqa: BLE001
if self._attr_available: if self._attr_available:
_LOGGER.warning( _LOGGER.warning(
@ -226,7 +231,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB
) )
self._attr_available = False self._attr_available = False
else: else:
self._attr_available = True self._attr_available = available
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:

View File

@ -106,7 +106,7 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity):
await self.fan_module.set_fan_speed_level(value_in_range) await self.fan_module.set_fan_speed_level(value_in_range)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
fan_speed = self.fan_module.fan_speed_level fan_speed = self.fan_module.fan_speed_level
self._attr_is_on = fan_speed != 0 self._attr_is_on = fan_speed != 0
@ -114,3 +114,4 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity):
self._attr_percentage = ranged_value_to_percentage(SPEED_RANGE, fan_speed) self._attr_percentage = ranged_value_to_percentage(SPEED_RANGE, fan_speed)
else: else:
self._attr_percentage = None self._attr_percentage = None
return True

View File

@ -335,7 +335,7 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
return ColorMode.HS return ColorMode.HS
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
light_module = self._light_module light_module = self._light_module
self._attr_is_on = light_module.state.light_on is True self._attr_is_on = light_module.state.light_on is True
@ -349,6 +349,8 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
hue, saturation, _ = light_module.hsv hue, saturation, _ = light_module.hsv
self._attr_hs_color = hue, saturation self._attr_hs_color = hue, saturation
return True
class TPLinkLightEffectEntity(TPLinkLightEntity): class TPLinkLightEffectEntity(TPLinkLightEntity):
"""Representation of a TPLink Smart Light Strip.""" """Representation of a TPLink Smart Light Strip."""
@ -368,7 +370,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity):
_attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
super()._async_update_attrs() super()._async_update_attrs()
effect_module = self._effect_module effect_module = self._effect_module
@ -381,6 +383,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity):
self._attr_effect_list = effect_list self._attr_effect_list = effect_list
else: else:
self._attr_effect_list = None self._attr_effect_list = None
return True
@async_refresh_after @async_refresh_after
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -114,6 +114,7 @@ class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity):
await self._feature.set_value(int(value)) await self._feature.set_value(int(value))
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_native_value = cast(float | None, self._feature.value) self._attr_native_value = cast(float | None, self._feature.value)
return True

View File

@ -91,6 +91,7 @@ class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity):
await self._feature.set_value(option) await self._feature.set_value(option)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_current_option = cast(str | None, self._feature.value) self._attr_current_option = cast(str | None, self._feature.value)
return True

View File

@ -153,7 +153,7 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
entity_description: TPLinkSensorEntityDescription entity_description: TPLinkSensorEntityDescription
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
value = self._feature.value value = self._feature.value
if value is not None and self._feature.precision_hint is not None: if value is not None and self._feature.precision_hint is not None:
@ -171,3 +171,4 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
# Map to homeassistant units and fallback to upstream one if none found # Map to homeassistant units and fallback to upstream one if none found
if (unit := self._feature.unit) is not None: if (unit := self._feature.unit) is not None:
self._attr_native_unit_of_measurement = UNIT_MAPPING.get(unit, unit) self._attr_native_unit_of_measurement = UNIT_MAPPING.get(unit, unit)
return True

View File

@ -56,6 +56,7 @@ class TPLinkSirenEntity(CoordinatedTPLinkEntity, SirenEntity):
await self._alarm_module.stop() await self._alarm_module.stop()
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_is_on = self._alarm_module.active self._attr_is_on = self._alarm_module.active
return True

View File

@ -109,6 +109,7 @@ class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity):
await self._feature.set_value(False) await self._feature.set_value(False)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> bool:
"""Update the entity's attributes.""" """Update the entity's attributes."""
self._attr_is_on = cast(bool | None, self._feature.value) self._attr_is_on = cast(bool | None, self._feature.value)
return True

View File

@ -1,7 +1,7 @@
{ {
"domain": "tuya", "domain": "tuya",
"name": "Tuya", "name": "Tuya",
"codeowners": ["@Tuya", "@zlinoliver", "@frenck"], "codeowners": ["@Tuya", "@zlinoliver"],
"config_flow": true, "config_flow": true,
"dependencies": ["ffmpeg"], "dependencies": ["ffmpeg"],
"dhcp": [ "dhcp": [

View File

@ -13,7 +13,7 @@
"velbus-packet", "velbus-packet",
"velbus-protocol" "velbus-protocol"
], ],
"requirements": ["velbus-aio==2024.12.2"], "requirements": ["velbus-aio==2024.12.3"],
"usb": [ "usb": [
{ {
"vid": "10CF", "vid": "10CF",

View File

@ -53,7 +53,9 @@ LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] =
ColorMode.COLOR_TEMP, ColorMode.COLOR_TEMP,
], ],
LightCapability.RGB_COLOR | LightCapability.COLOR_TEMPERATURE: [ LightCapability.RGB_COLOR | LightCapability.COLOR_TEMPERATURE: [
ColorMode.RGBWW, # Technically this is RGBWW but wled does not support RGBWW colors (with warm and cold white separately)
# but rather RGB + CCT which does not have a direct mapping in HA
ColorMode.RGB,
], ],
LightCapability.WHITE_CHANNEL | LightCapability.COLOR_TEMPERATURE: [ LightCapability.WHITE_CHANNEL | LightCapability.COLOR_TEMPERATURE: [
ColorMode.COLOR_TEMP, ColorMode.COLOR_TEMP,

View File

@ -10,11 +10,7 @@ from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import ( from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from . import api from . import api
from .const import DOMAIN from .const import DOMAIN
@ -40,9 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
) )
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth( auth = api.AsyncConfigEntryAuth(session)
aiohttp_client.async_get_clientsession(hass), session
)
client = XboxLiveClient(auth) client = XboxLiveClient(auth)
consoles: SmartglassConsoleList = await client.smartglass.get_console_list() consoles: SmartglassConsoleList = await client.smartglass.get_console_list()

View File

@ -1,24 +1,20 @@
"""API for xbox bound to Home Assistant OAuth.""" """API for xbox bound to Home Assistant OAuth."""
from aiohttp import ClientSession
from xbox.webapi.authentication.manager import AuthenticationManager from xbox.webapi.authentication.manager import AuthenticationManager
from xbox.webapi.authentication.models import OAuth2TokenResponse from xbox.webapi.authentication.models import OAuth2TokenResponse
from xbox.webapi.common.signed_session import SignedSession
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
class AsyncConfigEntryAuth(AuthenticationManager): class AsyncConfigEntryAuth(AuthenticationManager):
"""Provide xbox authentication tied to an OAuth2 based config entry.""" """Provide xbox authentication tied to an OAuth2 based config entry."""
def __init__( def __init__(self, oauth_session: OAuth2Session) -> None:
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize xbox auth.""" """Initialize xbox auth."""
# Leaving out client credentials as they are handled by Home Assistant # Leaving out client credentials as they are handled by Home Assistant
super().__init__(websession, "", "", "") super().__init__(SignedSession(), "", "", "")
self._oauth_session = oauth_session self._oauth_session = oauth_session
self.oauth = self._get_oauth_token() self.oauth = self._get_oauth_token()

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 1 MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0.dev0" PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"

View File

@ -2475,6 +2475,11 @@
"config_flow": false, "config_flow": false,
"iot_class": "local_polling" "iot_class": "local_polling"
}, },
"harvey": {
"name": "Harvey",
"integration_type": "virtual",
"supported_by": "aquacell"
},
"hassio": { "hassio": {
"name": "Home Assistant Supervisor", "name": "Home Assistant Supervisor",
"integration_type": "hub", "integration_type": "hub",

View File

@ -175,6 +175,9 @@ async def async_process_integration_platforms(
else: else:
integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS] integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS]
# Tell the loader that it should try to pre-load the integration
# for any future components that are loaded so we can reduce the
# amount of import executor usage.
async_register_preload_platform(hass, platform_name) async_register_preload_platform(hass, platform_name)
top_level_components = hass.config.top_level_components.copy() top_level_components = hass.config.top_level_components.copy()
process_job = HassJob( process_job = HassJob(
@ -187,10 +190,6 @@ async def async_process_integration_platforms(
integration_platform = IntegrationPlatform( integration_platform = IntegrationPlatform(
platform_name, process_job, top_level_components platform_name, process_job, top_level_components
) )
# Tell the loader that it should try to pre-load the integration
# for any future components that are loaded so we can reduce the
# amount of import executor usage.
async_register_preload_platform(hass, platform_name)
integration_platforms.append(integration_platform) integration_platforms.append(integration_platform)
if not top_level_components: if not top_level_components:
return return

View File

@ -65,20 +65,20 @@ _LOGGER = logging.getLogger(__name__)
# This list can be extended by calling async_register_preload_platform # This list can be extended by calling async_register_preload_platform
# #
BASE_PRELOAD_PLATFORMS = [ BASE_PRELOAD_PLATFORMS = [
"backup",
"config", "config",
"config_flow", "config_flow",
"diagnostics", "diagnostics",
"energy", "energy",
"group", "group",
"logbook",
"hardware", "hardware",
"intent", "intent",
"logbook",
"media_source", "media_source",
"recorder", "recorder",
"repairs", "repairs",
"system_health", "system_health",
"trigger", "trigger",
"backup",
] ]

View File

@ -25,6 +25,7 @@ bluetooth-data-tools==1.20.0
cached-ipaddress==0.8.0 cached-ipaddress==0.8.0
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.3.2 ciso8601==2.3.2
cronsim==2.6
cryptography==44.0.0 cryptography==44.0.0
dbus-fast==2.24.3 dbus-fast==2.24.3
fnv-hash-fast==1.0.2 fnv-hash-fast==1.0.2
@ -34,11 +35,11 @@ habluetooth==3.6.0
hass-nabucasa==0.87.0 hass-nabucasa==0.87.0
hassil==2.0.5 hassil==2.0.5
home-assistant-bluetooth==1.13.0 home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241127.8 home-assistant-frontend==20241223.1
home-assistant-intents==2024.12.20 home-assistant-intents==2024.12.20
httpx==0.27.2 httpx==0.27.2
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.5
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0 mutagen==1.47.0
orjson==3.10.12 orjson==3.10.12

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.1.0.dev0" version = "2025.2.0.dev0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -42,6 +42,7 @@ dependencies = [
"bcrypt==4.2.0", "bcrypt==4.2.0",
"certifi>=2021.5.30", "certifi>=2021.5.30",
"ciso8601==2.3.2", "ciso8601==2.3.2",
"cronsim==2.6",
"fnv-hash-fast==1.0.2", "fnv-hash-fast==1.0.2",
# hass-nabucasa is imported by helpers which don't depend on the cloud # hass-nabucasa is imported by helpers which don't depend on the cloud
# integration # integration
@ -51,7 +52,7 @@ dependencies = [
"httpx==0.27.2", "httpx==0.27.2",
"home-assistant-bluetooth==1.13.0", "home-assistant-bluetooth==1.13.0",
"ifaddr==0.2.0", "ifaddr==0.2.0",
"Jinja2==3.1.4", "Jinja2==3.1.5",
"lru-dict==1.3.0", "lru-dict==1.3.0",
"PyJWT==2.10.1", "PyJWT==2.10.1",
# PyJWT has loose dependency. We want the latest one. # PyJWT has loose dependency. We want the latest one.
@ -903,6 +904,7 @@ mark-parentheses = false
[tool.ruff.lint.flake8-tidy-imports.banned-api] [tool.ruff.lint.flake8-tidy-imports.banned-api]
"async_timeout".msg = "use asyncio.timeout instead" "async_timeout".msg = "use asyncio.timeout instead"
"pytz".msg = "use zoneinfo instead" "pytz".msg = "use zoneinfo instead"
"tests".msg = "You should not import tests"
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
force-sort-within-sections = true force-sort-within-sections = true

View File

@ -18,12 +18,13 @@ awesomeversion==24.6.0
bcrypt==4.2.0 bcrypt==4.2.0
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.3.2 ciso8601==2.3.2
cronsim==2.6
fnv-hash-fast==1.0.2 fnv-hash-fast==1.0.2
hass-nabucasa==0.87.0 hass-nabucasa==0.87.0
httpx==0.27.2 httpx==0.27.2
home-assistant-bluetooth==1.13.0 home-assistant-bluetooth==1.13.0
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.5
lru-dict==1.3.0 lru-dict==1.3.0
PyJWT==2.10.1 PyJWT==2.10.1
cryptography==44.0.0 cryptography==44.0.0

View File

@ -470,7 +470,7 @@ anthropic==0.31.2
apple_weatherkit==1.1.3 apple_weatherkit==1.1.3
# homeassistant.components.apprise # homeassistant.components.apprise
apprise==1.9.0 apprise==1.9.1
# homeassistant.components.aprs # homeassistant.components.aprs
aprslib==0.7.2 aprslib==0.7.2
@ -1134,7 +1134,7 @@ hole==0.8.0
holidays==0.63 holidays==0.63
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20241127.8 home-assistant-frontend==20241223.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.12.20 home-assistant-intents==2024.12.20
@ -1248,7 +1248,7 @@ justnimbus==0.7.4
kaiterra-async-client==1.0.0 kaiterra-async-client==1.0.0
# homeassistant.components.keba # homeassistant.components.keba
keba-kecontact==1.1.0 keba-kecontact==1.3.0
# homeassistant.components.kegtron # homeassistant.components.kegtron
kegtron-ble==0.4.0 kegtron-ble==0.4.0
@ -2951,7 +2951,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2 vehicle==2.2.2
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2024.12.2 velbus-aio==2024.12.3
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.19 venstarcolortouch==0.19

View File

@ -443,7 +443,7 @@ anthropic==0.31.2
apple_weatherkit==1.1.3 apple_weatherkit==1.1.3
# homeassistant.components.apprise # homeassistant.components.apprise
apprise==1.9.0 apprise==1.9.1
# homeassistant.components.aprs # homeassistant.components.aprs
aprslib==0.7.2 aprslib==0.7.2
@ -963,7 +963,7 @@ hole==0.8.0
holidays==0.63 holidays==0.63
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20241127.8 home-assistant-frontend==20241223.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2024.12.20 home-assistant-intents==2024.12.20
@ -2367,7 +2367,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2 vehicle==2.2.2
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2024.12.2 velbus-aio==2024.12.3
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.19 venstarcolortouch==0.19

View File

@ -5,3 +5,7 @@ extend = "../pyproject.toml"
forced-separate = [ forced-separate = [
"tests", "tests",
] ]
[lint.flake8-tidy-imports.banned-api]
"async_timeout".msg = "use asyncio.timeout instead"
"pytz".msg = "use zoneinfo instead"

View File

@ -17,6 +17,7 @@
'entry_id': '7442b231f139e813fc1939281123f220', 'entry_id': '7442b231f139e813fc1939281123f220',
'minor_version': 1, 'minor_version': 1,
'options': dict({ 'options': dict({
'radar_updates': True,
}), }),
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
@ -33,6 +34,12 @@
]), ]),
}), }),
'lib': dict({ 'lib': dict({
'radar': dict({
'datetime': '2021-01-09T11:34:06.448809+00:00',
'id': 'national',
'image-bytes': '**REDACTED**',
'image-type': 'image/gif',
}),
'station': dict({ 'station': dict({
'altitude': 667.0, 'altitude': 667.0,
'coordinates': '**REDACTED**', 'coordinates': '**REDACTED**',

View File

@ -6,7 +6,11 @@ from aemet_opendata.exceptions import AuthError
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN from homeassistant.components.aemet.const import (
CONF_RADAR_UPDATES,
CONF_STATION_UPDATES,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -61,13 +65,20 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("user_input", "expected"), [({}, True), ({CONF_STATION_UPDATES: False}, False)] ("user_input", "expected"),
[
({}, {CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: True}),
(
{CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: False},
{CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: False},
),
],
) )
async def test_form_options( async def test_form_options(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
user_input: dict[str, bool], user_input: dict[str, bool],
expected: bool, expected: dict[str, bool],
) -> None: ) -> None:
"""Test the form options.""" """Test the form options."""
@ -98,7 +109,8 @@ async def test_form_options(
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert entry.options == { assert entry.options == {
CONF_STATION_UPDATES: expected, CONF_RADAR_UPDATES: expected[CONF_RADAR_UPDATES],
CONF_STATION_UPDATES: expected[CONF_STATION_UPDATES],
} }
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -0,0 +1,22 @@
"""The image tests for the AEMET OpenData platform."""
from freezegun.api import FrozenDateTimeFactory
from homeassistant.core import HomeAssistant
from .util import async_init_integration
async def test_aemet_create_images(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test creation of AEMET images."""
await hass.config.async_set_time_zone("UTC")
freezer.move_to("2021-01-09 12:00:00+00:00")
await async_init_integration(hass)
state = hass.states.get("image.aemet_weather_radar")
assert state is not None
assert state.state == "2021-01-09T11:34:06.448809+00:00"

View File

@ -3,9 +3,9 @@
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
from aemet_opendata.const import ATTR_DATA from aemet_opendata.const import ATTR_BYTES, ATTR_DATA, ATTR_TIMESTAMP, ATTR_TYPE
from homeassistant.components.aemet.const import DOMAIN from homeassistant.components.aemet.const import CONF_RADAR_UPDATES, DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -19,6 +19,14 @@ FORECAST_HOURLY_DATA_MOCK = {
ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-hourly-data.json"), ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-hourly-data.json"),
} }
RADAR_DATA_MOCK = {
ATTR_DATA: {
ATTR_TYPE: "image/gif",
ATTR_BYTES: bytes([0]),
},
ATTR_TIMESTAMP: "2021-01-09T11:34:06.448809+00:00",
}
STATION_DATA_MOCK = { STATION_DATA_MOCK = {
ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"), ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"),
} }
@ -53,6 +61,9 @@ def mock_api_call(cmd: str, fetch_data: bool = False) -> dict[str, Any]:
return FORECAST_DAILY_DATA_MOCK return FORECAST_DAILY_DATA_MOCK
if cmd == "prediccion/especifica/municipio/horaria/28065": if cmd == "prediccion/especifica/municipio/horaria/28065":
return FORECAST_HOURLY_DATA_MOCK return FORECAST_HOURLY_DATA_MOCK
if cmd == "red/radar/nacional":
return RADAR_DATA_MOCK
return {} return {}
@ -69,6 +80,9 @@ async def async_init_integration(hass: HomeAssistant):
}, },
entry_id="7442b231f139e813fc1939281123f220", entry_id="7442b231f139e813fc1939281123f220",
unique_id="40.30403754--3.72935236", unique_id="40.30403754--3.72935236",
options={
CONF_RADAR_UPDATES: True,
},
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)

View File

@ -12,7 +12,7 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN, DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
) )
from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -46,6 +46,17 @@ async def test_number(
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == "50" assert state.state == "50"
mock_apsystems.get_max_power.side_effect = TimeoutError()
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data={ATTR_VALUE: 50.1},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
@pytest.mark.usefixtures("mock_apsystems") @pytest.mark.usefixtures("mock_apsystems")

View File

@ -0,0 +1,29 @@
{
"did": "8516fbb1-17f1-4194-0000001",
"name": "E1234567890000000003",
"class": "qhe2o2",
"resource": "NHl5",
"company": "eco-ng",
"bindTs": 1734792100110,
"service": {
"jmq": "jmq-ngiot-eu.dc.ww.ecouser.net",
"mqs": "api-ngiot.dc-eu.ww.ecouser.net"
},
"deviceName": "DEEBOT N20 PRO PLUS",
"icon": "https: //portal-ww.ecouser.net/api/pim/file/get/0000001",
"ota": true,
"UILogicId": "y2_ww_h_y2h5",
"materialNo": "110-2406-0001",
"pid": "0000001",
"product_category": "DEEBOT",
"model": "Y2_AES_BLACK_INT",
"updateInfo": {
"needUpdate": false,
"changeLog": ""
},
"nick": "Dusty",
"homeId": "1234567890abcdef12345678",
"homeSort": 1,
"status": 1,
"otaUpgrade": {}
}

View File

@ -91,6 +91,328 @@
'state': '2024-01-01T00:00:00+00:00', 'state': '2024-01-01T00:00:00+00:00',
}) })
# --- # ---
# name: test_buttons[qhe2o2][button.dusty_empty_dustbin:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.dusty_empty_dustbin',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Empty dustbin',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'station_action_empty_dustbin',
'unique_id': '8516fbb1-17f1-4194-0000001_station_action_empty_dustbin',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_empty_dustbin:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Empty dustbin',
}),
'context': <ANY>,
'entity_id': 'button.dusty_empty_dustbin',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_relocate:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_relocate',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Relocate',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'relocate',
'unique_id': '8516fbb1-17f1-4194-0000001_relocate',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_relocate:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Relocate',
}),
'context': <ANY>,
'entity_id': 'button.dusty_relocate',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_filter_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_reset_filter_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset filter lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_lifespan_filter',
'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_filter',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_filter_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Reset filter lifespan',
}),
'context': <ANY>,
'entity_id': 'button.dusty_reset_filter_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_main_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_reset_main_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset main brush lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_lifespan_brush',
'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_brush',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_main_brush_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Reset main brush lifespan',
}),
'context': <ANY>,
'entity_id': 'button.dusty_reset_main_brush_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_round_mop_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_reset_round_mop_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset round mop lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_lifespan_round_mop',
'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_round_mop',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_round_mop_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Reset round mop lifespan',
}),
'context': <ANY>,
'entity_id': 'button.dusty_reset_round_mop_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_side_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_reset_side_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset side brush lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_lifespan_side_brush',
'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_side_brush',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_side_brush_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Reset side brush lifespan',
}),
'context': <ANY>,
'entity_id': 'button.dusty_reset_side_brush_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_unit_care_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.dusty_reset_unit_care_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset unit care lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_lifespan_unit_care',
'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_unit_care',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[qhe2o2][button.dusty_reset_unit_care_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Reset unit care lifespan',
}),
'context': <ANY>,
'entity_id': 'button.dusty_reset_unit_care_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',
})
# ---
# name: test_buttons[yna5x1][button.ozmo_950_relocate:entity-registry] # name: test_buttons[yna5x1][button.ozmo_950_relocate:entity-registry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -725,6 +725,781 @@
'state': 'Testnetwork', 'state': 'Testnetwork',
}) })
# --- # ---
# name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_area_cleaned',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Area cleaned',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'stats_area',
'unique_id': '8516fbb1-17f1-4194-0000001_stats_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Area cleaned',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_area_cleaned',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_battery:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '8516fbb1-17f1-4194-0000001_battery_level',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_battery:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Dusty Battery',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_cleaning_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_cleaning_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Cleaning duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'stats_time',
'unique_id': '8516fbb1-17f1-4194-0000001_stats_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_cleaning_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Dusty Cleaning duration',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_cleaning_duration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_error:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Error',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'error',
'unique_id': '8516fbb1-17f1-4194-0000001_error',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_error:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'description': 'NoError: Robot is operational',
'friendly_name': 'Dusty Error',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_filter_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_filter_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Filter lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifespan_filter',
'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_filter',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_filter_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Filter lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_filter_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '56',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_ip_address:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_ip_address',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'IP address',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'network_ip',
'unique_id': '8516fbb1-17f1-4194-0000001_network_ip',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_ip_address:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty IP address',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_ip_address',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '192.168.0.10',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_main_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_main_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Main brush lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifespan_brush',
'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_brush',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_main_brush_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Main brush lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_main_brush_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_round_mop_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_round_mop_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Round mop lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifespan_round_mop',
'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_round_mop',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_round_mop_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Round mop lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_round_mop_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_side_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_side_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Side brush lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifespan_side_brush',
'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_side_brush',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_side_brush_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Side brush lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_side_brush_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '40',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_station_state:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'idle',
'emptying_dustbin',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_station_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Station state',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'station_state',
'unique_id': '8516fbb1-17f1-4194-0000001_station_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_station_state:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Dusty Station state',
'options': list([
'idle',
'emptying_dustbin',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_station_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'emptying_dustbin',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_total_area_cleaned',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total area cleaned',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_stats_area',
'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Total area cleaned',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_total_area_cleaned',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_cleaning_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_total_cleaning_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Total cleaning duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_stats_time',
'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_time',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_cleaning_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Dusty Total cleaning duration',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_total_cleaning_duration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '40.000',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_cleanings:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.dusty_total_cleanings',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total cleanings',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_stats_cleanings',
'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_cleanings',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_total_cleanings:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Total cleanings',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_total_cleanings',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_unit_care_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_unit_care_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Unit care lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifespan_unit_care',
'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_unit_care',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_unit_care_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Unit care lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_unit_care_lifespan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_rssi:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_wi_fi_rssi',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Wi-Fi RSSI',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'network_rssi',
'unique_id': '8516fbb1-17f1-4194-0000001_network_rssi',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_rssi:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Wi-Fi RSSI',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_wi_fi_rssi',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-62',
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_ssid:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.dusty_wi_fi_ssid',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Wi-Fi SSID',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'network_ssid',
'unique_id': '8516fbb1-17f1-4194-0000001_network_ssid',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_ssid:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Wi-Fi SSID',
}),
'context': <ANY>,
'entity_id': 'sensor.dusty_wi_fi_ssid',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Testnetwork',
})
# ---
# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:entity-registry] # name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:entity-registry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -1,7 +1,12 @@
"""Tests for Ecovacs sensors.""" """Tests for Ecovacs sensors."""
from deebot_client.command import Command from deebot_client.command import Command
from deebot_client.commands.json import ResetLifeSpan, SetRelocationState from deebot_client.commands import StationAction
from deebot_client.commands.json import (
ResetLifeSpan,
SetRelocationState,
station_action,
)
from deebot_client.events import LifeSpan from deebot_client.events import LifeSpan
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -60,8 +65,38 @@ def platforms() -> Platform | list[Platform]:
), ),
], ],
), ),
(
"qhe2o2",
[
("button.dusty_relocate", SetRelocationState()),
(
"button.dusty_reset_main_brush_lifespan",
ResetLifeSpan(LifeSpan.BRUSH),
),
(
"button.dusty_reset_filter_lifespan",
ResetLifeSpan(LifeSpan.FILTER),
),
(
"button.dusty_reset_side_brush_lifespan",
ResetLifeSpan(LifeSpan.SIDE_BRUSH),
),
(
"button.dusty_reset_unit_care_lifespan",
ResetLifeSpan(LifeSpan.UNIT_CARE),
),
(
"button.dusty_reset_round_mop_lifespan",
ResetLifeSpan(LifeSpan.ROUND_MOP),
),
(
"button.dusty_empty_dustbin",
station_action.StationAction(StationAction.EMPTY_DUSTBIN),
),
],
),
], ],
ids=["yna5x1", "5xu9h3"], ids=["yna5x1", "5xu9h3", "qhe2o2"],
) )
async def test_buttons( async def test_buttons(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -11,6 +11,7 @@ from deebot_client.events import (
NetworkInfoEvent, NetworkInfoEvent,
StatsEvent, StatsEvent,
TotalStatsEvent, TotalStatsEvent,
station,
) )
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -45,6 +46,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
event_bus.notify(LifeSpanEvent(LifeSpan.FILTER, 56, 40 * 60)) event_bus.notify(LifeSpanEvent(LifeSpan.FILTER, 56, 40 * 60))
event_bus.notify(LifeSpanEvent(LifeSpan.SIDE_BRUSH, 40, 20 * 60)) event_bus.notify(LifeSpanEvent(LifeSpan.SIDE_BRUSH, 40, 20 * 60))
event_bus.notify(ErrorEvent(0, "NoError: Robot is operational")) event_bus.notify(ErrorEvent(0, "NoError: Robot is operational"))
event_bus.notify(station.StationEvent(station.State.EMPTYING))
await block_till_done(hass, event_bus) await block_till_done(hass, event_bus)
@ -87,8 +89,29 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
"sensor.goat_g1_error", "sensor.goat_g1_error",
], ],
), ),
(
"qhe2o2",
[
"sensor.dusty_area_cleaned",
"sensor.dusty_cleaning_duration",
"sensor.dusty_total_area_cleaned",
"sensor.dusty_total_cleaning_duration",
"sensor.dusty_total_cleanings",
"sensor.dusty_battery",
"sensor.dusty_ip_address",
"sensor.dusty_wi_fi_rssi",
"sensor.dusty_wi_fi_ssid",
"sensor.dusty_station_state",
"sensor.dusty_main_brush_lifespan",
"sensor.dusty_filter_lifespan",
"sensor.dusty_side_brush_lifespan",
"sensor.dusty_unit_care_lifespan",
"sensor.dusty_round_mop_lifespan",
"sensor.dusty_error",
],
),
], ],
ids=["yna5x1", "5xu9h3"], ids=["yna5x1", "5xu9h3", "qhe2o2"],
) )
async def test_sensors( async def test_sensors(
hass: HomeAssistant, hass: HomeAssistant,
@ -99,7 +122,7 @@ async def test_sensors(
entity_ids: list[str], entity_ids: list[str],
) -> None: ) -> None:
"""Test that sensor entity snapshots match.""" """Test that sensor entity snapshots match."""
assert entity_ids == hass.states.async_entity_ids() assert hass.states.async_entity_ids() == entity_ids
for entity_id in entity_ids: for entity_id in entity_ids:
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN

View File

@ -1465,6 +1465,105 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None
assert hass.states.get("sensor.sensor4").state == "50.0" assert hass.states.get("sensor.sensor4").state == "50.0"
async def test_state_change_during_window_rollover(
recorder_mock: Recorder,
hass: HomeAssistant,
) -> None:
"""Test when the tracked sensor and the start/end window change during the same update."""
await hass.config.async_set_time_zone("UTC")
utcnow = dt_util.utcnow()
start_time = utcnow.replace(hour=23, minute=0, second=0, microsecond=0)
def _fake_states(*args, **kwargs):
return {
"binary_sensor.state": [
ha.State(
"binary_sensor.state",
"on",
last_changed=start_time - timedelta(hours=11),
last_updated=start_time - timedelta(hours=11),
),
]
}
# The test begins at 23:00, and queries from the database that the sensor has been on since 12:00.
with (
patch(
"homeassistant.components.recorder.history.state_changes_during_period",
_fake_states,
),
freeze_time(start_time),
):
await async_setup_component(
hass,
"sensor",
{
"sensor": [
{
"platform": "history_stats",
"entity_id": "binary_sensor.state",
"name": "sensor1",
"state": "on",
"start": "{{ today_at() }}",
"end": "{{ now() }}",
"type": "time",
}
]
},
)
await hass.async_block_till_done()
await async_update_entity(hass, "sensor.sensor1")
await hass.async_block_till_done()
assert hass.states.get("sensor.sensor1").state == "11.0"
# Advance 59 minutes, to record the last minute update just before midnight, just like a real system would do.
t2 = start_time + timedelta(minutes=59, microseconds=300)
with freeze_time(t2):
async_fire_time_changed(hass, t2)
await hass.async_block_till_done()
assert hass.states.get("sensor.sensor1").state == "11.98"
# One minute has passed and the time has now rolled over into a new day, resetting the recorder window. The sensor will then query the database for updates,
# and will see that the sensor is ON starting from midnight.
t3 = t2 + timedelta(minutes=1)
def _fake_states_t3(*args, **kwargs):
return {
"binary_sensor.state": [
ha.State(
"binary_sensor.state",
"on",
last_changed=t3.replace(hour=0, minute=0, second=0, microsecond=0),
last_updated=t3.replace(hour=0, minute=0, second=0, microsecond=0),
),
]
}
with (
patch(
"homeassistant.components.recorder.history.state_changes_during_period",
_fake_states_t3,
),
freeze_time(t3),
):
# The sensor turns off around this time, before the sensor does its normal polled update.
hass.states.async_set("binary_sensor.state", "off")
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get("sensor.sensor1").state == "0.0"
# More time passes, and the history stats does a polled update again. It should be 0 since the sensor has been off since midnight.
t4 = t3 + timedelta(minutes=10)
with freeze_time(t4):
async_fire_time_changed(hass, t4)
await hass.async_block_till_done()
assert hass.states.get("sensor.sensor1").state == "0.0"
@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"]) @pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"])
async def test_end_time_with_microseconds_zeroed( async def test_end_time_with_microseconds_zeroed(
time_zone: str, time_zone: str,

View File

@ -0,0 +1,12 @@
modbus:
type: "tcp"
host: "testHost"
port: 5001
name: "testModbus"
sensors:
- name: "dummy"
address: 117
slave: 0
- name: "dummy_2"
address: 118
slave: 1

View File

@ -25,7 +25,6 @@ import voluptuous as vol
from homeassistant import config as hass_config from homeassistant import config as hass_config
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.modbus import async_reset_platform
from homeassistant.components.modbus.const import ( from homeassistant.components.modbus.const import (
ATTR_ADDRESS, ATTR_ADDRESS,
ATTR_HUB, ATTR_HUB,
@ -1159,22 +1158,61 @@ async def test_integration_reload(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
mock_modbus, mock_modbus,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Run test for integration reload.""" """Run test for integration reload."""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
caplog.clear() caplog.clear()
yaml_path = get_fixture_path("configuration.yaml", "modbus") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
await hass.async_block_till_done()
yaml_path = get_fixture_path("configuration.yaml", DOMAIN)
with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done() await hass.async_block_till_done()
for _ in range(4):
freezer.tick(timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert "Modbus reloading" in caplog.text assert "Modbus reloading" in caplog.text
state_sensor_1 = hass.states.get("sensor.dummy")
state_sensor_2 = hass.states.get("sensor.dummy_2")
assert state_sensor_1
assert not state_sensor_2
caplog.clear()
yaml_path = get_fixture_path("configuration_2.yaml", DOMAIN)
with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert "Modbus reloading" in caplog.text
state_sensor_1 = hass.states.get("sensor.dummy")
state_sensor_2 = hass.states.get("sensor.dummy_2")
assert state_sensor_1
assert state_sensor_2
caplog.clear()
yaml_path = get_fixture_path("configuration_empty.yaml", DOMAIN)
with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert "Modbus not present anymore" in caplog.text
state_sensor_1 = hass.states.get("sensor.dummy")
state_sensor_2 = hass.states.get("sensor.dummy_2")
assert not state_sensor_1
assert not state_sensor_2
@pytest.mark.parametrize("do_config", [{}]) @pytest.mark.parametrize("do_config", [{}])
@ -1227,9 +1265,3 @@ async def test_no_entities(hass: HomeAssistant) -> None:
] ]
} }
assert await async_setup_component(hass, DOMAIN, config) is False assert await async_setup_component(hass, DOMAIN, config) is False
async def test_reset_platform(hass: HomeAssistant) -> None:
"""Run test for async_reset_platform."""
await async_reset_platform(hass, "modbus")
assert DOMAIN not in hass.data

View File

@ -1,5 +1,10 @@
"""Tests for the niko_home_control integration.""" """Tests for the niko_home_control integration."""
from collections.abc import Awaitable, Callable
from unittest.mock import AsyncMock
import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -11,3 +16,13 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
def find_update_callback(
mock: AsyncMock, identifier: int
) -> Callable[[int], Awaitable[None]]:
"""Find the update callback for a specific identifier."""
for call in mock.register_callback.call_args_list:
if call[0][0] == identifier:
return call[0][1]
pytest.fail(f"Callback for identifier {identifier} not found")

View File

@ -3,6 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from nhc.cover import NHCCover
from nhc.light import NHCLight from nhc.light import NHCLight
import pytest import pytest
@ -48,9 +49,21 @@ def dimmable_light() -> NHCLight:
return mock return mock
@pytest.fixture
def cover() -> NHCCover:
"""Return a cover mock."""
mock = AsyncMock(spec=NHCCover)
mock.id = 3
mock.type = 4
mock.name = "cover"
mock.suggested_area = "room"
mock.state = 100
return mock
@pytest.fixture @pytest.fixture
def mock_niko_home_control_connection( def mock_niko_home_control_connection(
light: NHCLight, dimmable_light: NHCLight light: NHCLight, dimmable_light: NHCLight, cover: NHCCover
) -> Generator[AsyncMock]: ) -> Generator[AsyncMock]:
"""Mock a NHC client.""" """Mock a NHC client."""
with ( with (
@ -65,6 +78,7 @@ def mock_niko_home_control_connection(
): ):
client = mock_client.return_value client = mock_client.return_value
client.lights = [light, dimmable_light] client.lights = [light, dimmable_light]
client.covers = [cover]
yield client yield client

View File

@ -0,0 +1,48 @@
# serializer version: 1
# name: test_cover[cover.cover-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.cover',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'niko_home_control',
'previous_unique_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-3',
'unit_of_measurement': None,
})
# ---
# name: test_cover[cover.cover-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'cover',
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.cover',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---

View File

@ -0,0 +1,138 @@
"""Tests for the Niko Home Control cover platform."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_STOP_COVER,
STATE_CLOSED,
STATE_OPEN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import find_update_callback, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_cover(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch(
"homeassistant.components.niko_home_control.PLATFORMS", [Platform.COVER]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("cover_id", "entity_id"),
[
(0, "cover.cover"),
],
)
async def test_open_cover(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
cover_id: int,
entity_id: int,
) -> None:
"""Test opening the cover."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mock_niko_home_control_connection.covers[cover_id].open.assert_called_once_with()
@pytest.mark.parametrize(
("cover_id", "entity_id"),
[
(0, "cover.cover"),
],
)
async def test_close_cover(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
cover_id: int,
entity_id: str,
) -> None:
"""Test closing the cover."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mock_niko_home_control_connection.covers[cover_id].close.assert_called_once_with()
@pytest.mark.parametrize(
("cover_id", "entity_id"),
[
(0, "cover.cover"),
],
)
async def test_stop_cover(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
cover_id: int,
entity_id: str,
) -> None:
"""Test closing the cover."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mock_niko_home_control_connection.covers[cover_id].stop.assert_called_once_with()
async def test_updating(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
cover: AsyncMock,
) -> None:
"""Test closing the cover."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("cover.cover").state == STATE_OPEN
cover.state = 0
await find_update_callback(mock_niko_home_control_connection, 3)(0)
await hass.async_block_till_done()
assert hass.states.get("cover.cover").state == STATE_CLOSED
cover.state = 100
await find_update_callback(mock_niko_home_control_connection, 3)(100)
await hass.async_block_till_done()
assert hass.states.get("cover.cover").state == STATE_OPEN

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import setup_integration from . import find_update_callback, setup_integration
from tests.common import MockConfigEntry, snapshot_platform from tests.common import MockConfigEntry, snapshot_platform
@ -113,7 +113,7 @@ async def test_updating(
assert hass.states.get("light.light").state == STATE_ON assert hass.states.get("light.light").state == STATE_ON
light.state = 0 light.state = 0
await mock_niko_home_control_connection.register_callback.call_args_list[0][0][1](0) await find_update_callback(mock_niko_home_control_connection, 1)(0)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.light").state == STATE_OFF assert hass.states.get("light.light").state == STATE_OFF
@ -122,16 +122,14 @@ async def test_updating(
assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255 assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255
dimmable_light.state = 80 dimmable_light.state = 80
await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1]( await find_update_callback(mock_niko_home_control_connection, 2)(80)
80
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.dimmable_light").state == STATE_ON assert hass.states.get("light.dimmable_light").state == STATE_ON
assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204 assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204
dimmable_light.state = 0 dimmable_light.state = 0
await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1](0) await find_update_callback(mock_niko_home_control_connection, 2)(0)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.dimmable_light").state == STATE_OFF assert hass.states.get("light.dimmable_light").state == STATE_OFF

View File

@ -23,7 +23,7 @@ async def test_entities(
"""Test the binary sensors entities.""" """Test the binary sensors entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )

View File

@ -34,7 +34,7 @@ async def test_entities(
"""Test the button entities.""" """Test the button entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )
@ -81,7 +81,7 @@ async def test_buttons(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An error occurred while communicating " r"An error occurred while communicating "
r"with the Peblar device: Could not connect" r"with the Peblar EV charger: Could not connect"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(
@ -101,7 +101,7 @@ async def test_buttons(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An unknown error occurred while communicating " r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error" r"with the Peblar EV charger: Unknown error"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(
@ -122,7 +122,7 @@ async def test_buttons(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An authentication failure occurred while communicating " r"An authentication failure occurred while communicating "
r"with the Peblar device" r"with the Peblar EV charger"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(

View File

@ -26,7 +26,7 @@ pytestmark = [
( (
PeblarConnectionError("Could not connect"), PeblarConnectionError("Could not connect"),
( (
"An error occurred while communicating with the Peblar device: " "An error occurred while communicating with the Peblar EV charger: "
"Could not connect" "Could not connect"
), ),
), ),
@ -34,7 +34,7 @@ pytestmark = [
PeblarError("Unknown error"), PeblarError("Unknown error"),
( (
"An unknown error occurred while communicating " "An unknown error occurred while communicating "
"with the Peblar device: Unknown error" "with the Peblar EV charger: Unknown error"
), ),
), ),
], ],

View File

@ -36,7 +36,7 @@ async def test_entities(
"""Test the number entities.""" """Test the number entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )
@ -80,7 +80,7 @@ async def test_number_set_value(
PeblarConnectionError("Could not connect"), PeblarConnectionError("Could not connect"),
( (
r"An error occurred while communicating " r"An error occurred while communicating "
r"with the Peblar device: Could not connect" r"with the Peblar EV charger: Could not connect"
), ),
"communication_error", "communication_error",
{"error": "Could not connect"}, {"error": "Could not connect"},
@ -89,7 +89,7 @@ async def test_number_set_value(
PeblarError("Unknown error"), PeblarError("Unknown error"),
( (
r"An unknown error occurred while communicating " r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error" r"with the Peblar EV charger: Unknown error"
), ),
"unknown_error", "unknown_error",
{"error": "Unknown error"}, {"error": "Unknown error"},
@ -143,7 +143,7 @@ async def test_number_set_value_authentication_error(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An authentication failure occurred while communicating " r"An authentication failure occurred while communicating "
r"with the Peblar device" r"with the Peblar EV charger"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(

View File

@ -41,7 +41,7 @@ async def test_entities(
"""Test the select entities.""" """Test the select entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )
@ -85,7 +85,7 @@ async def test_select_option(
PeblarConnectionError("Could not connect"), PeblarConnectionError("Could not connect"),
( (
r"An error occurred while communicating " r"An error occurred while communicating "
r"with the Peblar device: Could not connect" r"with the Peblar EV charger: Could not connect"
), ),
"communication_error", "communication_error",
{"error": "Could not connect"}, {"error": "Could not connect"},
@ -94,7 +94,7 @@ async def test_select_option(
PeblarError("Unknown error"), PeblarError("Unknown error"),
( (
r"An unknown error occurred while communicating " r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error" r"with the Peblar EV charger: Unknown error"
), ),
"unknown_error", "unknown_error",
{"error": "Unknown error"}, {"error": "Unknown error"},
@ -150,7 +150,7 @@ async def test_select_option_authentication_error(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An authentication failure occurred while communicating " r"An authentication failure occurred while communicating "
r"with the Peblar device" r"with the Peblar EV charger"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(

View File

@ -24,7 +24,7 @@ async def test_entities(
"""Test the sensor entities.""" """Test the sensor entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )

View File

@ -36,7 +36,7 @@ async def test_entities(
"""Test the switch entities.""" """Test the switch entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )
@ -88,7 +88,7 @@ async def test_switch(
PeblarConnectionError("Could not connect"), PeblarConnectionError("Could not connect"),
( (
r"An error occurred while communicating " r"An error occurred while communicating "
r"with the Peblar device: Could not connect" r"with the Peblar EV charger: Could not connect"
), ),
"communication_error", "communication_error",
{"error": "Could not connect"}, {"error": "Could not connect"},
@ -97,7 +97,7 @@ async def test_switch(
PeblarError("Unknown error"), PeblarError("Unknown error"),
( (
r"An unknown error occurred while communicating " r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error" r"with the Peblar EV charger: Unknown error"
), ),
"unknown_error", "unknown_error",
{"error": "Unknown error"}, {"error": "Unknown error"},
@ -152,7 +152,7 @@ async def test_switch_authentication_error(
HomeAssistantError, HomeAssistantError,
match=( match=(
r"An authentication failure occurred while communicating " r"An authentication failure occurred while communicating "
r"with the Peblar device" r"with the Peblar EV charger"
), ),
) as excinfo: ) as excinfo:
await hass.services.async_call( await hass.services.async_call(

View File

@ -23,7 +23,7 @@ async def test_entities(
"""Test the update entities.""" """Test the update entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device # Ensure all entities are correctly assigned to the Peblar EV charger
device_entry = device_registry.async_get_device( device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")} identifiers={(DOMAIN, "23-45-A4O-MOF")}
) )

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