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
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.1"
HA_SHORT_VERSION: "2025.2"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
# 10.3 is the oldest supported version

View File

@ -1103,8 +1103,8 @@ build.json @home-assistant/supervisor
/tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/homeassistant/components/overkiz/ @imicknl
/tests/components/overkiz/ @imicknl
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
@ -1135,8 +1135,8 @@ build.json @home-assistant/supervisor
/tests/components/plaato/ @JohNan
/homeassistant/components/plex/ @jjlawren
/tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
/tests/components/plugwise/ @CoMPaTech @bouwew
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike
@ -1573,8 +1573,8 @@ build.json @home-assistant/supervisor
/tests/components/triggercmd/ @rvmey
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
/tests/components/tuya/ @Tuya @zlinoliver @frenck
/homeassistant/components/tuya/ @Tuya @zlinoliver
/tests/components/tuya/ @Tuya @zlinoliver
/homeassistant/components/twentemilieu/ @frenck
/tests/components/twentemilieu/ @frenck
/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.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
_LOGGER = logging.getLogger(__name__)
@ -26,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
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):
update_features |= UpdateFeature.STATION

View File

@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
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(
{
vol.Required(CONF_RADAR_UPDATES, default=False): 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
ATTRIBUTION = "Powered by AEMET OpenData"
CONF_RADAR_UPDATES = "radar_updates"
CONF_STATION_UPDATES = "station_updates"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
DEFAULT_NAME = "AEMET"
DOMAIN = "aemet"

View File

@ -4,7 +4,7 @@ from __future__ import annotations
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.const import (
@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
TO_REDACT_COORD = [
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": {
"step": {
"init": {
"data": {
"radar_updates": "Gather data from AEMET weather radar",
"station_updates": "Gather data from AEMET weather stations"
}
}

View File

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

View File

@ -2,6 +2,8 @@
from __future__ import annotations
from aiohttp import ClientConnectorError
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant
@ -45,7 +47,13 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
async def async_update(self) -> None:
"""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:
"""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.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 (
BackupAgent,
BackupAgentError,

View File

@ -36,7 +36,14 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
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 .const import (
CONF_ACCOUNT_LINK_SERVER,

View File

@ -2,7 +2,12 @@
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 homeassistant.components.button import ButtonEntity, ButtonEntityDescription
@ -11,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
from .entity import (
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
@ -35,6 +40,13 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription):
component: LifeSpan
@dataclass(kw_only=True, frozen=True)
class EcovacsStationActionButtonEntityDescription(ButtonEntityDescription):
"""Ecovacs station action button entity description."""
action: StationAction
ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = (
EcovacsButtonEntityDescription(
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(
EcovacsLifespanButtonEntityDescription(
component=component,
@ -74,6 +96,15 @@ async def async_setup_entry(
for description in LIFESPAN_ENTITY_DESCRIPTIONS
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)
@ -103,3 +134,18 @@ class EcovacsResetLifespanButtonEntity(
await self._device.execute_command(
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 deebot_client.commands import StationAction
from deebot_client.events import LifeSpan
DOMAIN = "ecovacs"
@ -19,8 +20,11 @@ SUPPORTED_LIFESPANS = (
LifeSpan.SIDE_BRUSH,
LifeSpan.UNIT_CARE,
LifeSpan.ROUND_MOP,
LifeSpan.STATION_FILTER,
)
SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,)
LEGACY_SUPPORTED_LIFESPANS = (
"main_brush",
"side_brush",

View File

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

View File

@ -16,6 +16,7 @@ from deebot_client.events import (
NetworkInfoEvent,
StatsEvent,
TotalStatsEvent,
station,
)
from sucks import VacBot
@ -46,7 +47,7 @@ from .entity import (
EcovacsLegacyEntity,
EventT,
)
from .util import get_supported_entitites
from .util import get_name_key, get_options, get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@ -136,6 +137,15 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
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": {
"name": "Relocate"
},
"reset_lifespan_base_station_filter": {
"name": "Reset station filter lifespan"
},
"reset_lifespan_blade": {
"name": "Reset blade lifespan"
},
@ -66,6 +69,9 @@
},
"reset_lifespan_side_brush": {
"name": "Reset side brush lifespan"
},
"station_action_empty_dustbin": {
"name": "Empty dustbin"
}
},
"event": {
@ -107,6 +113,9 @@
}
}
},
"lifespan_base_station_filter": {
"name": "Station filter lifespan"
},
"lifespan_blade": {
"name": "Blade lifespan"
},
@ -140,6 +149,13 @@
"network_ssid": {
"name": "Wi-Fi SSID"
},
"station_state": {
"name": "Station state",
"state": {
"idle": "[%key:common::state::idle%]",
"emptying_dustbin": "Emptying dustbin"
}
},
"stats_area": {
"name": "Area cleaned"
},

View File

@ -7,6 +7,8 @@ import random
import string
from typing import TYPE_CHECKING
from deebot_client.events.station import State
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify
@ -47,4 +49,13 @@ def get_supported_entitites(
@callback
def get_name_key(enum: Enum) -> str:
"""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()
@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-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: >

View File

@ -27,6 +27,7 @@ class FritzboxCoordinatorData:
devices: dict[str, FritzhomeDevice]
templates: dict[str, FritzhomeTemplate]
supported_color_properties: dict[str, tuple[dict, list]]
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
@ -49,7 +50,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.new_devices: set[str] = set()
self.new_templates: set[str] = set()
self.data = FritzboxCoordinatorData({}, {})
self.data = FritzboxCoordinatorData({}, {}, {})
async def async_setup(self) -> None:
"""Set up the coordinator."""
@ -120,6 +121,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
devices = self.fritz.get_devices()
device_data = {}
supported_color_properties = self.data.supported_color_properties
for device in devices:
# assume device as unavailable, see #55799
if (
@ -136,6 +138,13 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
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 = {}
if self.has_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_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:
"""Fetch all device data."""

View File

@ -57,7 +57,6 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
) -> None:
"""Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None)
self._supported_hs: dict[int, list[int]] = {}
self._attr_supported_color_modes = {ColorMode.ONOFF}
if self.data.has_color:
@ -65,6 +64,26 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
elif self.data.has_level:
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
def is_on(self) -> bool:
"""If the light is currently on or off."""
@ -148,30 +167,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off)
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",
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "gold",
"requirements": ["PyFronius==0.7.3"]
}

View File

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

View File

@ -77,7 +77,7 @@
},
"error": {
"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%]",
"unable_still_load": "[%key:component::generic::config::error::unable_still_load%]",
"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
):
self._history_current_period.append(
HistoryState(
new_state.state, new_state.last_changed.timestamp()
)
HistoryState(new_state.state, new_state.last_changed_timestamp)
)
new_data = True
if not new_data and current_period_end_timestamp < now_timestamp:
@ -131,6 +129,16 @@ class HistoryStats:
await self._async_history_from_db(
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
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)
self.device = await self.hive.sensor.getSensor(self.device)
self.attributes = self.device.get("attributes", {})
self._attr_is_on = self.device["status"]["state"]
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:
self._attr_available = True
if self._attr_available:
self._attr_is_on = self.device["status"].get("state")
class HiveSensorEntity(HiveEntity, BinarySensorEntity):
"""Hive Sensor Entity."""

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["keba_kecontact"],
"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_UNIQUE_ID,
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
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 .const import (
@ -451,18 +455,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Modbus component."""
if DOMAIN not in config:
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(
hass,
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.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType
from .const import (
@ -125,8 +124,6 @@ async def async_modbus_setup(
) -> bool:
"""Set up Modbus component."""
await async_setup_reload_service(hass, DOMAIN, [DOMAIN])
if config[DOMAIN]:
config[DOMAIN] = check_config(hass, config[DOMAIN])
if not config[DOMAIN]:

View File

@ -13,7 +13,7 @@ from homeassistant.helpers import entity_registry as er
from .const import _LOGGER
PLATFORMS: list[Platform] = [Platform.LIGHT]
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
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",
"name": "Overkiz",
"codeowners": [
"@imicknl",
"@vlebourl",
"@tetienne",
"@nyroDev",
"@tronix117",
"@alexfp14"
],
"codeowners": ["@imicknl"],
"config_flow": true,
"dhcp": [
{

View File

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

View File

@ -20,7 +20,7 @@
"data_description": {
"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": {
"data": {
@ -31,7 +31,7 @@
"host": "[%key:component::peblar::config::step::user::data_description::host%]",
"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": {
"data": {
@ -39,10 +39,10 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Peblar charger on your home network.",
"password": "The same password as you use to log in to the Peblar device' local web interface."
"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 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": {
"data": {
@ -51,7 +51,7 @@
"data_description": {
"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": {
"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": {
"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": {
"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",
"name": "Plugwise",
"codeowners": ["@CoMPaTech", "@bouwew", "@frenck"],
"codeowners": ["@CoMPaTech", "@bouwew"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"integration_type": "hub",

View File

@ -28,7 +28,14 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
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
CONF_DB_INTEGRITY_CHECK,
DOMAIN,

View File

@ -115,7 +115,7 @@
},
"firmware_update": {
"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": {
"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._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]
@ -137,8 +137,8 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
def preset_mode(self) -> str:
"""Return current/last preset mode."""
if self.hvac_mode == HVACMode.OFF:
return HEAT_MODE(self._last_preset).title
return HEAT_MODE(self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE]).title
return HEAT_MODE(self._last_preset).name.lower()
return HEAT_MODE(self.entity_data[VALUE.HEAT_MODE][ATTR.VALUE]).name.lower()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Change the setpoint of the heater."""

View File

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

View File

@ -3,8 +3,9 @@
"service_config_entry_name": "Config entry",
"service_config_entry_description": "The config entry to use for this action.",
"climate_preset_solar": "Solar",
"climate_preset_solar_prefered": "Solar Prefered",
"climate_preset_heater": "Heater"
"climate_preset_solar_preferred": "Solar Preferred",
"climate_preset_heater": "Heater",
"climate_preset_dont_change": "Don't Change"
},
"config": {
"flow_title": "{name}",
@ -133,10 +134,30 @@
},
"climate": {
"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": {
"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": {
@ -191,7 +212,12 @@
"name": "[%key:component::screenlogic::entity::number::salt_tds_ppm::name%]"
},
"chem_dose_state": {
"name": "{chem} dosing state"
"name": "{chem} dosing state",
"state": {
"dosing": "Dosing",
"mixing": "Mixing",
"monitoring": "Monitoring"
}
},
"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:
"""Set up Subaru from a config entry."""
config = entry.data
websession = aiohttp_client.async_get_clientsession(hass)
websession = aiohttp_client.async_create_clientsession(hass)
try:
controller = SubaruAPI(
websession,

View File

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

View File

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

View File

@ -52,15 +52,19 @@ BUTTON_DESCRIPTIONS: Final = [
),
TPLinkButtonEntityDescription(
key="pan_left",
available_fn=lambda dev: dev.is_on,
),
TPLinkButtonEntityDescription(
key="pan_right",
available_fn=lambda dev: dev.is_on,
),
TPLinkButtonEntityDescription(
key="tilt_up",
available_fn=lambda dev: dev.is_on,
),
TPLinkButtonEntityDescription(
key="tilt_down",
available_fn=lambda dev: dev.is_on,
),
]
@ -100,5 +104,6 @@ class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity):
"""Execute action."""
await self._feature.set_value(True)
def _async_update_attrs(self) -> None:
def _async_update_attrs(self) -> bool:
"""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 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 homeassistant.components import ffmpeg, stream
@ -40,6 +40,7 @@ CAMERA_DESCRIPTIONS: tuple[TPLinkCameraEntityDescription, ...] = (
TPLinkCameraEntityDescription(
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."""
self.entity_description = description
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
super().__init__(device, coordinator, parent=parent)
Camera.__init__(self)
@ -108,16 +111,19 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
def _get_unique_id(self) -> str:
"""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
def _async_update_attrs(self) -> None:
def _async_update_attrs(self) -> bool:
"""Update the entity's attributes."""
self._attr_is_on = self._camera_module.is_on
return self.entity_description.available_fn(self._device)
async def stream_source(self) -> str | None:
"""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:
"""Check for an auth error and start reauth flow."""
@ -148,7 +154,7 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
return self._image
# 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
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)
@callback
def _async_update_attrs(self) -> None:
def _async_update_attrs(self) -> bool:
"""Update the entity's attributes."""
self._attr_current_temperature = cast(float | None, self._temp_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._attr_hvac_action = HVACAction.OFF
return
return True
self._attr_hvac_action = STATE_TO_ACTION[
cast(ThermostatState, self._mode_feature.value)
]
return True
def _get_unique_id(self) -> str:
"""Return unique id."""

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,9 @@ LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] =
ColorMode.COLOR_TEMP,
],
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: [
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.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from . import api
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)
auth = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
auth = api.AsyncConfigEntryAuth(session)
client = XboxLiveClient(auth)
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()

View File

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

View File

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

View File

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

View File

@ -175,6 +175,9 @@ async def async_process_integration_platforms(
else:
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)
top_level_components = hass.config.top_level_components.copy()
process_job = HassJob(
@ -187,10 +190,6 @@ async def async_process_integration_platforms(
integration_platform = IntegrationPlatform(
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)
if not top_level_components:
return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,3 +5,7 @@ extend = "../pyproject.toml"
forced-separate = [
"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',
'minor_version': 1,
'options': dict({
'radar_updates': True,
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
@ -33,6 +34,12 @@
]),
}),
'lib': dict({
'radar': dict({
'datetime': '2021-01-09T11:34:06.448809+00:00',
'id': 'national',
'image-bytes': '**REDACTED**',
'image-type': 'image/gif',
}),
'station': dict({
'altitude': 667.0,
'coordinates': '**REDACTED**',

View File

@ -6,7 +6,11 @@ from aemet_opendata.exceptions import AuthError
from freezegun.api import FrozenDateTimeFactory
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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
@ -61,13 +65,20 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
@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(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
user_input: dict[str, bool],
expected: bool,
expected: dict[str, bool],
) -> None:
"""Test the form options."""
@ -98,7 +109,8 @@ async def test_form_options(
assert result["type"] is FlowResultType.CREATE_ENTRY
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()

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 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.core import HomeAssistant
@ -19,6 +19,14 @@ FORECAST_HOURLY_DATA_MOCK = {
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 = {
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
if cmd == "prediccion/especifica/municipio/horaria/28065":
return FORECAST_HOURLY_DATA_MOCK
if cmd == "red/radar/nacional":
return RADAR_DATA_MOCK
return {}
@ -69,6 +80,9 @@ async def async_init_integration(hass: HomeAssistant):
},
entry_id="7442b231f139e813fc1939281123f220",
unique_id="40.30403754--3.72935236",
options={
CONF_RADAR_UPDATES: True,
},
)
config_entry.add_to_hass(hass)

View File

@ -12,7 +12,7 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
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.helpers import entity_registry as er
@ -46,6 +46,17 @@ async def test_number(
await hass.async_block_till_done()
state = hass.states.get(entity_id)
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")

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',
})
# ---
# 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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -725,6 +725,781 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -1,7 +1,12 @@
"""Tests for Ecovacs sensors."""
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
import pytest
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(
hass: HomeAssistant,

View File

@ -11,6 +11,7 @@ from deebot_client.events import (
NetworkInfoEvent,
StatsEvent,
TotalStatsEvent,
station,
)
import pytest
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.SIDE_BRUSH, 40, 20 * 60))
event_bus.notify(ErrorEvent(0, "NoError: Robot is operational"))
event_bus.notify(station.StationEvent(station.State.EMPTYING))
await block_till_done(hass, event_bus)
@ -87,8 +89,29 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
"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(
hass: HomeAssistant,
@ -99,7 +122,7 @@ async def test_sensors(
entity_ids: list[str],
) -> None:
"""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:
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
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"
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"])
async def test_end_time_with_microseconds_zeroed(
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.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.modbus import async_reset_platform
from homeassistant.components.modbus.const import (
ATTR_ADDRESS,
ATTR_HUB,
@ -1159,22 +1158,61 @@ async def test_integration_reload(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
mock_modbus,
freezer: FrozenDateTimeFactory,
) -> None:
"""Run test for integration reload."""
caplog.set_level(logging.DEBUG)
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):
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()
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
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", [{}])
@ -1227,9 +1265,3 @@ async def test_no_entities(hass: HomeAssistant) -> None:
]
}
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."""
from collections.abc import Awaitable, Callable
from unittest.mock import AsyncMock
import pytest
from homeassistant.core import HomeAssistant
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.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 unittest.mock import AsyncMock, patch
from nhc.cover import NHCCover
from nhc.light import NHCLight
import pytest
@ -48,9 +49,21 @@ def dimmable_light() -> NHCLight:
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
def mock_niko_home_control_connection(
light: NHCLight, dimmable_light: NHCLight
light: NHCLight, dimmable_light: NHCLight, cover: NHCCover
) -> Generator[AsyncMock]:
"""Mock a NHC client."""
with (
@ -65,6 +78,7 @@ def mock_niko_home_control_connection(
):
client = mock_client.return_value
client.lights = [light, dimmable_light]
client.covers = [cover]
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.helpers import entity_registry as er
from . import setup_integration
from . import find_update_callback, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@ -113,7 +113,7 @@ async def test_updating(
assert hass.states.get("light.light").state == STATE_ON
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()
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
dimmable_light.state = 80
await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1](
80
)
await find_update_callback(mock_niko_home_control_connection, 2)(80)
await hass.async_block_till_done()
assert hass.states.get("light.dimmable_light").state == STATE_ON
assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204
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()
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."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)

View File

@ -34,7 +34,7 @@ async def test_entities(
"""Test the button entities."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)
@ -81,7 +81,7 @@ async def test_buttons(
HomeAssistantError,
match=(
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:
await hass.services.async_call(
@ -101,7 +101,7 @@ async def test_buttons(
HomeAssistantError,
match=(
r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error"
r"with the Peblar EV charger: Unknown error"
),
) as excinfo:
await hass.services.async_call(
@ -122,7 +122,7 @@ async def test_buttons(
HomeAssistantError,
match=(
r"An authentication failure occurred while communicating "
r"with the Peblar device"
r"with the Peblar EV charger"
),
) as excinfo:
await hass.services.async_call(

View File

@ -26,7 +26,7 @@ pytestmark = [
(
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"
),
),
@ -34,7 +34,7 @@ pytestmark = [
PeblarError("Unknown error"),
(
"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."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)
@ -80,7 +80,7 @@ async def test_number_set_value(
PeblarConnectionError("Could not connect"),
(
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",
{"error": "Could not connect"},
@ -89,7 +89,7 @@ async def test_number_set_value(
PeblarError("Unknown error"),
(
r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error"
r"with the Peblar EV charger: Unknown error"
),
"unknown_error",
{"error": "Unknown error"},
@ -143,7 +143,7 @@ async def test_number_set_value_authentication_error(
HomeAssistantError,
match=(
r"An authentication failure occurred while communicating "
r"with the Peblar device"
r"with the Peblar EV charger"
),
) as excinfo:
await hass.services.async_call(

View File

@ -41,7 +41,7 @@ async def test_entities(
"""Test the select entities."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)
@ -85,7 +85,7 @@ async def test_select_option(
PeblarConnectionError("Could not connect"),
(
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",
{"error": "Could not connect"},
@ -94,7 +94,7 @@ async def test_select_option(
PeblarError("Unknown error"),
(
r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error"
r"with the Peblar EV charger: Unknown error"
),
"unknown_error",
{"error": "Unknown error"},
@ -150,7 +150,7 @@ async def test_select_option_authentication_error(
HomeAssistantError,
match=(
r"An authentication failure occurred while communicating "
r"with the Peblar device"
r"with the Peblar EV charger"
),
) as excinfo:
await hass.services.async_call(

View File

@ -24,7 +24,7 @@ async def test_entities(
"""Test the sensor entities."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)

View File

@ -36,7 +36,7 @@ async def test_entities(
"""Test the switch entities."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)
@ -88,7 +88,7 @@ async def test_switch(
PeblarConnectionError("Could not connect"),
(
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",
{"error": "Could not connect"},
@ -97,7 +97,7 @@ async def test_switch(
PeblarError("Unknown error"),
(
r"An unknown error occurred while communicating "
r"with the Peblar device: Unknown error"
r"with the Peblar EV charger: Unknown error"
),
"unknown_error",
{"error": "Unknown error"},
@ -152,7 +152,7 @@ async def test_switch_authentication_error(
HomeAssistantError,
match=(
r"An authentication failure occurred while communicating "
r"with the Peblar device"
r"with the Peblar EV charger"
),
) as excinfo:
await hass.services.async_call(

View File

@ -23,7 +23,7 @@ async def test_entities(
"""Test the update entities."""
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(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)

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