mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge branch 'dev' of github.com:home-assistant/core into zha_3phase_current_entities
This commit is contained in:
commit
109a6a428d
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -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
|
||||
|
12
CODEOWNERS
12
CODEOWNERS
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
86
homeassistant/components/aemet/image.py
Normal file
86
homeassistant/components/aemet/image.py
Normal 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)
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["apprise"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["apprise==1.9.0"]
|
||||
"requirements": ["apprise==1.9.1"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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]
|
||||
|
@ -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: >
|
||||
|
@ -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."""
|
||||
|
@ -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]),
|
||||
]
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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%]",
|
||||
|
1
homeassistant/components/harvey/__init__.py
Normal file
1
homeassistant/components/harvey/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Virtual integration: Harvey."""
|
6
homeassistant/components/harvey/manifest.json
Normal file
6
homeassistant/components/harvey/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "harvey",
|
||||
"name": "Harvey",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "aquacell"
|
||||
}
|
@ -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(
|
||||
|
@ -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."""
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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]:
|
||||
|
@ -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]
|
||||
|
||||
|
54
homeassistant/components/niko_home_control/cover.py
Normal file
54
homeassistant/components/niko_home_control/cover.py
Normal 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
|
@ -1,14 +1,7 @@
|
||||
{
|
||||
"domain": "overkiz",
|
||||
"name": "Overkiz",
|
||||
"codeowners": [
|
||||
"@imicknl",
|
||||
"@vlebourl",
|
||||
"@tetienne",
|
||||
"@nyroDev",
|
||||
"@tronix117",
|
||||
"@alexfp14"
|
||||
],
|
||||
"codeowners": ["@imicknl"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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."""
|
||||
|
@ -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"},
|
||||
),
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "tuya",
|
||||
"name": "Tuya",
|
||||
"codeowners": ["@Tuya", "@zlinoliver", "@frenck"],
|
||||
"codeowners": ["@Tuya", "@zlinoliver"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg"],
|
||||
"dhcp": [
|
||||
|
@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2024.12.2"],
|
||||
"requirements": ["velbus-aio==2024.12.3"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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}"
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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**',
|
||||
|
@ -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()
|
||||
|
22
tests/components/aemet/test_image.py
Normal file
22
tests/components/aemet/test_image.py
Normal 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"
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
29
tests/components/ecovacs/fixtures/devices/qhe2o2/device.json
Normal file
29
tests/components/ecovacs/fixtures/devices/qhe2o2/device.json
Normal 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": {}
|
||||
}
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
12
tests/components/modbus/fixtures/configuration_2.yaml
Normal file
12
tests/components/modbus/fixtures/configuration_2.yaml
Normal 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
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
48
tests/components/niko_home_control/snapshots/test_cover.ambr
Normal file
48
tests/components/niko_home_control/snapshots/test_cover.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
138
tests/components/niko_home_control/test_cover.py
Normal file
138
tests/components/niko_home_control/test_cover.py
Normal 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
|
@ -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
|
||||
|
@ -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")}
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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")}
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user