mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
2025.6.2 (#147355)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Joostlek <joostlek@outlook.com> Co-authored-by: Brian Rogers <brg468@hotmail.com> Co-authored-by: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Co-authored-by: starkillerOG <starkiller.og@gmail.com> Co-authored-by: Andre Lengwenus <alengwenus@gmail.com> Co-authored-by: Chris Talkington <chris@talkingtontech.com> Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com> Co-authored-by: elmurato <1382097+elmurato@users.noreply.github.com> Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com> Co-authored-by: Hessel <hesselonline@users.noreply.github.com> Co-authored-by: Ernst Klamer <e.klamer@gmail.com> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Ravaka Razafimanantsoa <3774520+SeraphicRav@users.noreply.github.com> Co-authored-by: Allen Porter <allen.porter@gmail.com> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: Brett Adams <Bre77@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: hahn-th <15319212+hahn-th@users.noreply.github.com> Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Joakim Sørensen <joasoe@proton.me> Co-authored-by: Michael Hansen <mike@rhasspy.org> Fix blocking open in Minecraft Server (#146820) Fix missing key for ecosmart in older Wallbox models (#146847) Fix device type filtering in sensor (#146945) Fix incorrect use of zip in service.async_get_all_descriptions (#147013) Fix Shelly entity names for gen1 sleeping devices (#147019) Fix log in onedrive (#147029) Fix Charge Cable binary sensor in Teslemetry (#147136) fix too many requests by API (#147197) Fix reload for Shelly devices with no script support (#147344)
This commit is contained in:
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 2
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"integrations": ["switchbot", "switchbot_cloud"]
|
||||
"integrations": ["switchbot", "switchbot_cloud"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.2"]
|
||||
"requirements": ["aioamazondevices==3.1.14"]
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.12.4"]
|
||||
"requirements": ["bthome-ble==3.13.1"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.1",
|
||||
"aioesphomeapi==33.0.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.75", "babel==2.15.0"]
|
||||
}
|
||||
|
@ -21,6 +21,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"requirements": ["aiohomeconnect==0.18.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.5"]
|
||||
"requirements": ["homematicip==2.0.6"]
|
||||
}
|
||||
|
@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
"requirements": ["pylamarzocco==2.0.9"]
|
||||
}
|
||||
|
@ -58,6 +58,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER]
|
||||
).target_temperature
|
||||
),
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="smart_standby_time",
|
||||
@ -221,7 +225,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
|
||||
entity_description: LaMarzoccoNumberEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | int:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.native_value_fn(self.coordinator.device)
|
||||
|
||||
|
@ -57,6 +57,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="steam_boiler_ready_time",
|
||||
|
@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.8.6", "lcn-frontend==0.2.5"]
|
||||
"requirements": ["pypck==0.8.8", "lcn-frontend==0.2.5"]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import dns.asyncresolver
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_dnspython_rdata_classes() -> None:
|
||||
"""Load dnspython rdata classes used by mcstatus."""
|
||||
def prevent_dnspython_blocking_operations() -> None:
|
||||
"""Prevent dnspython blocking operations by pre-loading required data."""
|
||||
|
||||
# Blocking import: https://github.com/rthalley/dnspython/issues/1083
|
||||
for rdtype in dns.rdatatype.RdataType:
|
||||
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
|
||||
dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
|
||||
|
||||
# Blocking open: https://github.com/rthalley/dnspython/issues/1200
|
||||
dns.asyncresolver.get_default_resolver()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
|
||||
await hass.async_add_executor_job(load_dnspython_rdata_classes)
|
||||
await hass.async_add_executor_job(prevent_dnspython_blocking_operations)
|
||||
|
||||
# Create coordinator instance and store it.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
|
@ -62,6 +62,7 @@ TILT_DEVICE_MAP = {
|
||||
BlindType.VerticalBlind: CoverDeviceClass.BLIND,
|
||||
BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND,
|
||||
BlindType.VerticalBlindRight: CoverDeviceClass.BLIND,
|
||||
BlindType.RollerTiltMotor: CoverDeviceClass.BLIND,
|
||||
}
|
||||
|
||||
TILT_ONLY_DEVICE_MAP = {
|
||||
|
@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.27"]
|
||||
"requirements": ["motionblinds==0.6.28"]
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]):
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except OneDriveException as err:
|
||||
_LOGGER.debug("Failed to fetch drive data: %s", err, exc_info=True)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_failed"
|
||||
) from err
|
||||
|
@ -16,10 +16,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
KEY_ADDRESS,
|
||||
KEY_DURATION_SECONDS,
|
||||
KEY_ID,
|
||||
KEY_LOCALITY,
|
||||
KEY_PROGRAM_ID,
|
||||
KEY_PROGRAM_NAME,
|
||||
KEY_RUN_SUMMARIES,
|
||||
@ -65,7 +63,6 @@ class RachioCalendarEntity(
|
||||
super().__init__(coordinator)
|
||||
self.base_station = base_station
|
||||
self._event: CalendarEvent | None = None
|
||||
self._location = coordinator.base_station[KEY_ADDRESS][KEY_LOCALITY]
|
||||
self._attr_translation_placeholders = {
|
||||
"base": coordinator.base_station[KEY_SERIAL_NUMBER]
|
||||
}
|
||||
@ -87,7 +84,6 @@ class RachioCalendarEntity(
|
||||
end=dt_util.as_local(start_time)
|
||||
+ timedelta(seconds=int(event[KEY_TOTAL_RUN_DURATION])),
|
||||
description=valves,
|
||||
location=self._location,
|
||||
)
|
||||
|
||||
def _handle_upcoming_event(self) -> dict[str, Any] | None:
|
||||
@ -155,7 +151,6 @@ class RachioCalendarEntity(
|
||||
start=event_start,
|
||||
end=event_end,
|
||||
description=valves,
|
||||
location=self._location,
|
||||
uid=f"{run[KEY_PROGRAM_ID]}/{run[KEY_START_TIME]}",
|
||||
)
|
||||
event_list.append(event)
|
||||
|
@ -75,8 +75,6 @@ KEY_PROGRAM_ID = "programId"
|
||||
KEY_PROGRAM_NAME = "programName"
|
||||
KEY_PROGRAM_RUN_SUMMARIES = "valveProgramRunSummaries"
|
||||
KEY_TOTAL_RUN_DURATION = "totalRunDurationSeconds"
|
||||
KEY_ADDRESS = "address"
|
||||
KEY_LOCALITY = "locality"
|
||||
KEY_SKIP = "skip"
|
||||
KEY_SKIPPABLE = "skippable"
|
||||
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
||||
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
|
||||
name=self._host.api.nvr_name,
|
||||
model=self._host.api.model,
|
||||
model_id=self._host.api.item_number,
|
||||
model_id=self._host.api.item_number(),
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
hw_version=self._host.api.hardware_version,
|
||||
sw_version=self._host.api.sw_version,
|
||||
|
@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.5"]
|
||||
"requirements": ["reolink-aio==0.14.1"]
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["rokuecp"],
|
||||
"requirements": ["rokuecp==0.19.3"],
|
||||
"requirements": ["rokuecp==0.19.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "roku:ecp",
|
||||
|
@ -235,11 +235,15 @@ class ShellyButton(ShellyBaseButton):
|
||||
self._attr_unique_id = f"{coordinator.mac}_{description.key}"
|
||||
if isinstance(coordinator, ShellyBlockCoordinator):
|
||||
self._attr_device_info = get_block_device_info(
|
||||
coordinator.device, coordinator.mac
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = get_rpc_device_info(
|
||||
coordinator.device, coordinator.mac
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
|
||||
|
@ -211,7 +211,10 @@ class BlockSleepingClimate(
|
||||
elif entry is not None:
|
||||
self._unique_id = entry.unique_id
|
||||
self._attr_device_info = get_block_device_info(
|
||||
coordinator.device, coordinator.mac, sensor_block
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
sensor_block,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_name = get_block_entity_name(
|
||||
self.coordinator.device, sensor_block, None
|
||||
|
@ -31,7 +31,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@ -114,6 +118,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
self.device = device
|
||||
self.device_id: str | None = None
|
||||
self._pending_platforms: list[Platform] | None = None
|
||||
self.suggested_area: str | None = None
|
||||
device_name = device.name if device.initialized else entry.title
|
||||
interval_td = timedelta(seconds=update_interval)
|
||||
# The device has come online at least once. In the case of a sleeping RPC
|
||||
@ -176,6 +181,11 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
hw_version=f"gen{get_device_entry_gen(self.config_entry)}",
|
||||
configuration_url=f"http://{get_host(self.config_entry.data[CONF_HOST])}:{get_http_port(self.config_entry.data)}",
|
||||
)
|
||||
# We want to use the main device area as the suggested area for sub-devices.
|
||||
if (area_id := device_entry.area_id) is not None:
|
||||
area_registry = ar.async_get(self.hass)
|
||||
if (area := area_registry.async_get_area(area_id)) is not None:
|
||||
self.suggested_area = area.name
|
||||
self.device_id = device_entry.id
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
@ -825,6 +835,15 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
except InvalidAuthError:
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
return
|
||||
except RpcCallError as err:
|
||||
# Ignore 404 (No handler for) error
|
||||
if err.code != 404:
|
||||
LOGGER.debug(
|
||||
"Error during shutdown for device %s: %s",
|
||||
self.name,
|
||||
err.message,
|
||||
)
|
||||
return
|
||||
except DeviceConnectionError as err:
|
||||
# If the device is restarting or has gone offline before
|
||||
# the ping/pong timeout happens, the shutdown command
|
||||
|
@ -362,7 +362,10 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
|
||||
self.block = block
|
||||
self._attr_name = get_block_entity_name(coordinator.device, block)
|
||||
self._attr_device_info = get_block_device_info(
|
||||
coordinator.device, coordinator.mac, block
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
block,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{block.description}"
|
||||
|
||||
@ -405,7 +408,10 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
|
||||
super().__init__(coordinator)
|
||||
self.key = key
|
||||
self._attr_device_info = get_rpc_device_info(
|
||||
coordinator.device, coordinator.mac, key
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
key,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{key}"
|
||||
self._attr_name = get_rpc_entity_name(coordinator.device, key)
|
||||
@ -521,7 +527,9 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{attribute}"
|
||||
self._attr_device_info = get_block_device_info(
|
||||
coordinator.device, coordinator.mac
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._last_value = None
|
||||
|
||||
@ -630,7 +638,10 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_device_info = get_block_device_info(
|
||||
coordinator.device, coordinator.mac, block
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
block,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
|
||||
if block is not None:
|
||||
@ -642,7 +653,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
|
||||
)
|
||||
elif entry is not None:
|
||||
self._attr_unique_id = entry.unique_id
|
||||
self._attr_name = cast(str, entry.original_name)
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
@ -698,7 +708,10 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity):
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_device_info = get_rpc_device_info(
|
||||
coordinator.device, coordinator.mac, key
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
key,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_unique_id = self._attr_unique_id = (
|
||||
f"{coordinator.mac}-{key}-{attribute}"
|
||||
|
@ -207,7 +207,10 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
|
||||
super().__init__(coordinator)
|
||||
self.event_id = int(key.split(":")[-1])
|
||||
self._attr_device_info = get_rpc_device_info(
|
||||
coordinator.device, coordinator.mac, key
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
key,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{key}"
|
||||
self._attr_name = get_rpc_entity_name(coordinator.device, key)
|
||||
|
@ -139,7 +139,11 @@ class RpcEmeterPhaseSensor(RpcSensor):
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
self._attr_device_info = get_rpc_device_info(
|
||||
coordinator.device, coordinator.mac, key, description.emeter_phase
|
||||
coordinator.device,
|
||||
coordinator.mac,
|
||||
key,
|
||||
emeter_phase=description.emeter_phase,
|
||||
suggested_area=coordinator.suggested_area,
|
||||
)
|
||||
|
||||
|
||||
|
@ -751,6 +751,7 @@ def get_rpc_device_info(
|
||||
mac: str,
|
||||
key: str | None = None,
|
||||
emeter_phase: str | None = None,
|
||||
suggested_area: str | None = None,
|
||||
) -> DeviceInfo:
|
||||
"""Return device info for RPC device."""
|
||||
if key is None:
|
||||
@ -770,6 +771,7 @@ def get_rpc_device_info(
|
||||
identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")},
|
||||
name=get_rpc_sub_device_name(device, key, emeter_phase),
|
||||
manufacturer="Shelly",
|
||||
suggested_area=suggested_area,
|
||||
via_device=(DOMAIN, mac),
|
||||
)
|
||||
|
||||
@ -784,6 +786,7 @@ def get_rpc_device_info(
|
||||
identifiers={(DOMAIN, f"{mac}-{key}")},
|
||||
name=get_rpc_sub_device_name(device, key),
|
||||
manufacturer="Shelly",
|
||||
suggested_area=suggested_area,
|
||||
via_device=(DOMAIN, mac),
|
||||
)
|
||||
|
||||
@ -805,7 +808,10 @@ def get_blu_trv_device_info(
|
||||
|
||||
|
||||
def get_block_device_info(
|
||||
device: BlockDevice, mac: str, block: Block | None = None
|
||||
device: BlockDevice,
|
||||
mac: str,
|
||||
block: Block | None = None,
|
||||
suggested_area: str | None = None,
|
||||
) -> DeviceInfo:
|
||||
"""Return device info for Block device."""
|
||||
if (
|
||||
@ -820,6 +826,7 @@ def get_block_device_info(
|
||||
identifiers={(DOMAIN, f"{mac}-{block.description}")},
|
||||
name=get_block_sub_device_name(device, block),
|
||||
manufacturer="Shelly",
|
||||
suggested_area=suggested_area,
|
||||
via_device=(DOMAIN, mac),
|
||||
)
|
||||
|
||||
|
@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.2.4"]
|
||||
"requirements": ["pysmartthings==3.2.5"]
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ async def async_setup_entry(
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[
|
||||
device.device_type
|
||||
]
|
||||
if device.device_type in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES
|
||||
)
|
||||
|
||||
|
||||
|
@ -151,6 +151,7 @@ async def async_setup_entry(
|
||||
SwitchBotCloudSensor(data.api, device, coordinator, description)
|
||||
for device, coordinator in data.devices.sensors
|
||||
for description in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]
|
||||
if device.device_type in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES
|
||||
)
|
||||
|
||||
|
||||
|
@ -126,7 +126,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
polling=True,
|
||||
polling_value_fn=lambda x: x != "<invalid>",
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType(
|
||||
lambda value: callback(value != "Unknown")
|
||||
lambda value: callback(value is not None and value != "Unknown")
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Support for Traccar Client."""
|
||||
|
||||
from http import HTTPStatus
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -20,7 +23,6 @@ from .const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_SPEED,
|
||||
ATTR_TIMESTAMP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@ -29,6 +31,7 @@ PLATFORMS = [Platform.DEVICE_TRACKER]
|
||||
|
||||
TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ACCURACY = 200
|
||||
DEFAULT_BATTERY = -1
|
||||
@ -49,21 +52,50 @@ WEBHOOK_SCHEMA = vol.Schema(
|
||||
vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float),
|
||||
vol.Optional(ATTR_BEARING): vol.Coerce(float),
|
||||
vol.Optional(ATTR_SPEED): vol.Coerce(float),
|
||||
vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def _parse_json_body(json_body: dict) -> dict:
|
||||
"""Parse JSON body from request."""
|
||||
location = json_body.get("location", {})
|
||||
coords = location.get("coords", {})
|
||||
battery_level = location.get("battery", {}).get("level")
|
||||
return {
|
||||
"id": json_body.get("device_id"),
|
||||
"lat": coords.get("latitude"),
|
||||
"lon": coords.get("longitude"),
|
||||
"accuracy": coords.get("accuracy"),
|
||||
"altitude": coords.get("altitude"),
|
||||
"batt": battery_level * 100 if battery_level is not None else DEFAULT_BATTERY,
|
||||
"bearing": coords.get("heading"),
|
||||
"speed": coords.get("speed"),
|
||||
}
|
||||
|
||||
|
||||
async def handle_webhook(
|
||||
hass: HomeAssistant, webhook_id: str, request: web.Request
|
||||
hass: HomeAssistant,
|
||||
webhook_id: str,
|
||||
request: web.Request,
|
||||
) -> web.Response:
|
||||
"""Handle incoming webhook with Traccar Client request."""
|
||||
if not (requestdata := dict(request.query)):
|
||||
try:
|
||||
requestdata = _parse_json_body(await request.json())
|
||||
except JSONDecodeError as error:
|
||||
LOGGER.error("Error parsing JSON body: %s", error)
|
||||
return web.Response(
|
||||
text="Invalid JSON",
|
||||
status=HTTPStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
try:
|
||||
data = WEBHOOK_SCHEMA(dict(request.query))
|
||||
data = WEBHOOK_SCHEMA(requestdata)
|
||||
except vol.MultipleInvalid as error:
|
||||
LOGGER.warning(humanize_error(requestdata, error))
|
||||
return web.Response(
|
||||
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
text=error.error_message,
|
||||
status=HTTPStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
attrs = {
|
||||
|
@ -17,7 +17,6 @@ ATTR_LONGITUDE = "lon"
|
||||
ATTR_MOTION = "motion"
|
||||
ATTR_SPEED = "speed"
|
||||
ATTR_STATUS = "status"
|
||||
ATTR_TIMESTAMP = "timestamp"
|
||||
ATTR_TRACKER = "tracker"
|
||||
ATTR_TRACCAR_ID = "traccar_id"
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.11.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.14.1", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@ -3,7 +3,7 @@
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "wallbox"
|
||||
UPDATE_INTERVAL = 30
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]
|
||||
|
||||
@ -74,3 +74,4 @@ class EcoSmartMode(StrEnum):
|
||||
OFF = "off"
|
||||
ECO_MODE = "eco_mode"
|
||||
FULL_SOLAR = "full_solar"
|
||||
DISABLED = "disabled"
|
||||
|
@ -90,7 +90,9 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P](
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
|
||||
raise ConfigEntryAuthFailed from wallbox_connection_error
|
||||
raise ConnectionError from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
return require_authentication
|
||||
|
||||
@ -137,49 +139,65 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@_require_authentication
|
||||
def _get_data(self) -> dict[str, Any]:
|
||||
"""Get new sensor data for Wallbox component."""
|
||||
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
|
||||
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY
|
||||
]
|
||||
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_LOCKED_UNLOCKED_KEY
|
||||
]
|
||||
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_ENERGY_PRICE_KEY
|
||||
]
|
||||
# Only show max_icp_current if power_boost is available in the wallbox unit:
|
||||
if (
|
||||
data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0
|
||||
and CHARGER_POWER_BOOST_KEY
|
||||
in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY]
|
||||
):
|
||||
data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_MAX_ICP_CURRENT_KEY
|
||||
try:
|
||||
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
|
||||
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY
|
||||
]
|
||||
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_LOCKED_UNLOCKED_KEY
|
||||
]
|
||||
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_ENERGY_PRICE_KEY
|
||||
]
|
||||
# Only show max_icp_current if power_boost is available in the wallbox unit:
|
||||
if (
|
||||
data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0
|
||||
and CHARGER_POWER_BOOST_KEY
|
||||
in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY]
|
||||
):
|
||||
data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
||||
CHARGER_MAX_ICP_CURRENT_KEY
|
||||
]
|
||||
|
||||
data[CHARGER_CURRENCY_KEY] = (
|
||||
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
|
||||
)
|
||||
data[CHARGER_CURRENCY_KEY] = (
|
||||
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
|
||||
)
|
||||
|
||||
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
|
||||
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
|
||||
)
|
||||
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
|
||||
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
|
||||
)
|
||||
|
||||
# Set current solar charging mode
|
||||
eco_smart_enabled = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][
|
||||
CHARGER_ECO_SMART_STATUS_KEY
|
||||
]
|
||||
eco_smart_mode = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][
|
||||
CHARGER_ECO_SMART_MODE_KEY
|
||||
]
|
||||
if eco_smart_enabled is False:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
|
||||
elif eco_smart_mode == 0:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
|
||||
elif eco_smart_mode == 1:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
|
||||
# Set current solar charging mode
|
||||
eco_smart_enabled = (
|
||||
data[CHARGER_DATA_KEY]
|
||||
.get(CHARGER_ECO_SMART_KEY, {})
|
||||
.get(CHARGER_ECO_SMART_STATUS_KEY)
|
||||
)
|
||||
|
||||
return data
|
||||
eco_smart_mode = (
|
||||
data[CHARGER_DATA_KEY]
|
||||
.get(CHARGER_ECO_SMART_KEY, {})
|
||||
.get(CHARGER_ECO_SMART_MODE_KEY)
|
||||
)
|
||||
if eco_smart_mode is None:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED
|
||||
elif eco_smart_enabled is False:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
|
||||
elif eco_smart_mode == 0:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
|
||||
elif eco_smart_mode == 1:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
|
||||
|
||||
return data # noqa: TRY300
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get new sensor data for Wallbox component."""
|
||||
@ -193,7 +211,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 403:
|
||||
raise InvalidAuth from wallbox_connection_error
|
||||
raise
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_set_charging_current(self, charging_current: float) -> None:
|
||||
"""Set maximum charging current for Wallbox."""
|
||||
@ -210,7 +234,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 403:
|
||||
raise InvalidAuth from wallbox_connection_error
|
||||
raise
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_set_icp_current(self, icp_current: float) -> None:
|
||||
"""Set maximum icp current for Wallbox."""
|
||||
@ -220,8 +250,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@_require_authentication
|
||||
def _set_energy_cost(self, energy_cost: float) -> None:
|
||||
"""Set energy cost for Wallbox."""
|
||||
|
||||
self._wallbox.setEnergyCost(self._station, energy_cost)
|
||||
try:
|
||||
self._wallbox.setEnergyCost(self._station, energy_cost)
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_set_energy_cost(self, energy_cost: float) -> None:
|
||||
"""Set energy cost for Wallbox."""
|
||||
@ -239,7 +277,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 403:
|
||||
raise InvalidAuth from wallbox_connection_error
|
||||
raise
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_set_lock_unlock(self, lock: bool) -> None:
|
||||
"""Set wallbox to locked or unlocked."""
|
||||
@ -249,11 +293,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@_require_authentication
|
||||
def _pause_charger(self, pause: bool) -> None:
|
||||
"""Set wallbox to pause or resume."""
|
||||
|
||||
if pause:
|
||||
self._wallbox.pauseChargingSession(self._station)
|
||||
else:
|
||||
self._wallbox.resumeChargingSession(self._station)
|
||||
try:
|
||||
if pause:
|
||||
self._wallbox.pauseChargingSession(self._station)
|
||||
else:
|
||||
self._wallbox.resumeChargingSession(self._station)
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_pause_charger(self, pause: bool) -> None:
|
||||
"""Set wallbox to pause or resume."""
|
||||
@ -263,13 +315,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@_require_authentication
|
||||
def _set_eco_smart(self, option: str) -> None:
|
||||
"""Set wallbox solar charging mode."""
|
||||
|
||||
if option == EcoSmartMode.ECO_MODE:
|
||||
self._wallbox.enableEcoSmart(self._station, 0)
|
||||
elif option == EcoSmartMode.FULL_SOLAR:
|
||||
self._wallbox.enableEcoSmart(self._station, 1)
|
||||
else:
|
||||
self._wallbox.disableEcoSmart(self._station)
|
||||
try:
|
||||
if option == EcoSmartMode.ECO_MODE:
|
||||
self._wallbox.enableEcoSmart(self._station, 0)
|
||||
elif option == EcoSmartMode.FULL_SOLAR:
|
||||
self._wallbox.enableEcoSmart(self._station, 1)
|
||||
else:
|
||||
self._wallbox.disableEcoSmart(self._station)
|
||||
except requests.exceptions.HTTPError as wallbox_connection_error:
|
||||
if wallbox_connection_error.response.status_code == 429:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="too_many_requests"
|
||||
) from wallbox_connection_error
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_failed"
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def async_set_eco_smart(self, option: str) -> None:
|
||||
"""Set wallbox solar charging mode."""
|
||||
|
@ -7,7 +7,7 @@ from typing import Any
|
||||
from homeassistant.components.lock import LockEntity, LockEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@ -41,7 +41,7 @@ async def async_setup_entry(
|
||||
)
|
||||
except InvalidAuth:
|
||||
return
|
||||
except ConnectionError as exc:
|
||||
except HomeAssistantError as exc:
|
||||
raise PlatformNotReady from exc
|
||||
|
||||
async_add_entities(
|
||||
|
@ -12,7 +12,7 @@ from typing import cast
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@ -93,7 +93,7 @@ async def async_setup_entry(
|
||||
)
|
||||
except InvalidAuth:
|
||||
return
|
||||
except ConnectionError as exc:
|
||||
except HomeAssistantError as exc:
|
||||
raise PlatformNotReady from exc
|
||||
|
||||
async_add_entities(
|
||||
|
@ -63,15 +63,15 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Create wallbox select entities in HASS."""
|
||||
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
WallboxSelect(coordinator, description)
|
||||
for ent in coordinator.data
|
||||
if (
|
||||
(description := SELECT_TYPES.get(ent))
|
||||
and description.supported_fn(coordinator)
|
||||
if coordinator.data[CHARGER_ECO_SMART_KEY] != EcoSmartMode.DISABLED:
|
||||
async_add_entities(
|
||||
WallboxSelect(coordinator, description)
|
||||
for ent in coordinator.data
|
||||
if (
|
||||
(description := SELECT_TYPES.get(ent))
|
||||
and description.supported_fn(coordinator)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WallboxSelect(WallboxEntity, SelectEntity):
|
||||
|
@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@ -49,11 +48,6 @@ from .const import (
|
||||
from .coordinator import WallboxCoordinator
|
||||
from .entity import WallboxEntity
|
||||
|
||||
CHARGER_STATION = "station"
|
||||
UPDATE_INTERVAL = 30
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WallboxSensorEntityDescription(SensorEntityDescription):
|
||||
|
@ -112,6 +112,9 @@
|
||||
"exceptions": {
|
||||
"api_failed": {
|
||||
"message": "Error communicating with Wallbox API"
|
||||
},
|
||||
"too_many_requests": {
|
||||
"message": "Error communicating with Wallbox API, too many requests"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.74"]
|
||||
"requirements": ["holidays==0.75"]
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.59"],
|
||||
"requirements": ["zha==0.0.60"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@ -318,12 +318,37 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = {
|
||||
|
||||
|
||||
# Mappings for boolean sensors
|
||||
BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = {
|
||||
CommandClass.BATTERY: BinarySensorEntityDescription(
|
||||
key=str(CommandClass.BATTERY),
|
||||
BOOLEAN_SENSOR_MAPPINGS: dict[tuple[int, int | str], BinarySensorEntityDescription] = {
|
||||
(CommandClass.BATTERY, "backup"): BinarySensorEntityDescription(
|
||||
key="battery_backup",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(CommandClass.BATTERY, "disconnected"): BinarySensorEntityDescription(
|
||||
key="battery_disconnected",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(CommandClass.BATTERY, "isLow"): BinarySensorEntityDescription(
|
||||
key="battery_is_low",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
(CommandClass.BATTERY, "lowFluid"): BinarySensorEntityDescription(
|
||||
key="battery_low_fluid",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(CommandClass.BATTERY, "overheating"): BinarySensorEntityDescription(
|
||||
key="battery_overheating",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(CommandClass.BATTERY, "rechargeable"): BinarySensorEntityDescription(
|
||||
key="battery_rechargeable",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -432,8 +457,9 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = self.generate_name(include_value_name=True)
|
||||
primary_value = self.info.primary_value
|
||||
if description := BOOLEAN_SENSOR_MAPPINGS.get(
|
||||
self.info.primary_value.command_class
|
||||
(primary_value.command_class, primary_value.property_)
|
||||
):
|
||||
self.entity_description = description
|
||||
|
||||
|
@ -139,7 +139,10 @@ ATTR_TWIST_ASSIST = "twist_assist"
|
||||
ADDON_SLUG = "core_zwave_js"
|
||||
|
||||
# Sensor entity description constants
|
||||
ENTITY_DESC_KEY_BATTERY = "battery"
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL = "battery_level"
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE = "battery_list_state"
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY = "battery_maximum_capacity"
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE = "battery_temperature"
|
||||
ENTITY_DESC_KEY_CURRENT = "current"
|
||||
ENTITY_DESC_KEY_VOLTAGE = "voltage"
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT = "energy_measurement"
|
||||
|
@ -896,6 +896,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=False,
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# generic text sensors
|
||||
ZWaveDiscoverySchema(
|
||||
@ -912,7 +913,6 @@ DISCOVERY_SCHEMAS = [
|
||||
hint="numeric_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.BATTERY,
|
||||
CommandClass.ENERGY_PRODUCTION,
|
||||
CommandClass.SENSOR_ALARM,
|
||||
CommandClass.SENSOR_MULTILEVEL,
|
||||
@ -921,6 +921,36 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
hint="numeric_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.BATTERY},
|
||||
type={ValueType.NUMBER},
|
||||
property={"level", "maximumCapacity"},
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
hint="numeric_sensor",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.BATTERY},
|
||||
type={ValueType.NUMBER},
|
||||
property={"temperature"},
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
hint="list",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.BATTERY},
|
||||
type={ValueType.NUMBER},
|
||||
property={"chargingStatus", "rechargeOrReplace"},
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
),
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
hint="numeric_sensor",
|
||||
@ -932,6 +962,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Meter sensors for Meter CC
|
||||
ZWaveDiscoverySchema(
|
||||
@ -957,6 +988,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# button for Indicator CC
|
||||
ZWaveDiscoverySchema(
|
||||
@ -980,6 +1012,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# binary switch
|
||||
# barrier operator signaling states
|
||||
@ -1184,6 +1217,7 @@ DISCOVERY_SCHEMAS = [
|
||||
any_available_states={(0, "idle")},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# event
|
||||
# stateful = False
|
||||
|
@ -133,7 +133,10 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ENTITY_DESC_KEY_BATTERY,
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE,
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_CO,
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
@ -380,8 +383,31 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
|
||||
def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData:
|
||||
"""Resolve helper class data for a discovered value."""
|
||||
|
||||
if value.command_class == CommandClass.BATTERY:
|
||||
return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE)
|
||||
if value.command_class == CommandClass.BATTERY and value.property_ == "level":
|
||||
return NumericSensorDataTemplateData(
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE
|
||||
)
|
||||
if value.command_class == CommandClass.BATTERY and value.property_ in (
|
||||
"chargingStatus",
|
||||
"rechargeOrReplace",
|
||||
):
|
||||
return NumericSensorDataTemplateData(
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE, None
|
||||
)
|
||||
if (
|
||||
value.command_class == CommandClass.BATTERY
|
||||
and value.property_ == "maximumCapacity"
|
||||
):
|
||||
return NumericSensorDataTemplateData(
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE
|
||||
)
|
||||
if (
|
||||
value.command_class == CommandClass.BATTERY
|
||||
and value.property_ == "temperature"
|
||||
):
|
||||
return NumericSensorDataTemplateData(
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
if value.command_class == CommandClass.METER:
|
||||
try:
|
||||
|
@ -58,7 +58,10 @@ from .const import (
|
||||
ATTR_VALUE,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
ENTITY_DESC_KEY_BATTERY,
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE,
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_CO,
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
@ -95,17 +98,33 @@ from .migrate import async_migrate_statistics_sensors
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
# These descriptions should include device class.
|
||||
ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
|
||||
tuple[str, str], SensorEntityDescription
|
||||
] = {
|
||||
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY,
|
||||
# These descriptions should have a non None unit of measurement.
|
||||
ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = {
|
||||
(ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
(ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
(ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
@ -285,8 +304,14 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
|
||||
),
|
||||
}
|
||||
|
||||
# These descriptions are without device class.
|
||||
# These descriptions are without unit of measurement.
|
||||
ENTITY_DESCRIPTION_KEY_MAP = {
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE: SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_LIST_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ENTITY_DESC_KEY_CO: SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@ -538,7 +563,7 @@ def get_entity_description(
|
||||
"""Return the entity description for the given data."""
|
||||
data_description_key = data.entity_description_key or ""
|
||||
data_unit = data.unit_of_measurement or ""
|
||||
return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get(
|
||||
return ENTITY_DESCRIPTION_KEY_UNIT_MAP.get(
|
||||
(data_description_key, data_unit),
|
||||
ENTITY_DESCRIPTION_KEY_MAP.get(
|
||||
data_description_key,
|
||||
@ -588,6 +613,10 @@ async def async_setup_entry(
|
||||
entities.append(
|
||||
ZWaveListSensor(config_entry, driver, info, entity_description)
|
||||
)
|
||||
elif info.platform_hint == "list":
|
||||
entities.append(
|
||||
ZWaveListSensor(config_entry, driver, info, entity_description)
|
||||
)
|
||||
elif info.platform_hint == "config_parameter":
|
||||
entities.append(
|
||||
ZWaveConfigParameterSensor(
|
||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
@ -6426,7 +6426,10 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "SwitchBot Cloud"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iot_standards": [
|
||||
"matter"
|
||||
]
|
||||
},
|
||||
"switcher_kis": {
|
||||
"name": "Switcher",
|
||||
|
@ -682,9 +682,12 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T
|
||||
|
||||
def _load_services_files(
|
||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
||||
) -> list[JSON_TYPE]:
|
||||
) -> dict[str, JSON_TYPE]:
|
||||
"""Load service files for multiple integrations."""
|
||||
return [_load_services_file(hass, integration) for integration in integrations]
|
||||
return {
|
||||
integration.domain: _load_services_file(hass, integration)
|
||||
for integration in integrations
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
@ -744,10 +747,9 @@ async def async_get_all_descriptions(
|
||||
_LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc)
|
||||
|
||||
if integrations:
|
||||
contents = await hass.async_add_executor_job(
|
||||
loaded = await hass.async_add_executor_job(
|
||||
_load_services_files, hass, integrations
|
||||
)
|
||||
loaded = dict(zip(domains_with_missing_services, contents, strict=False))
|
||||
|
||||
# Load translations for all service domains
|
||||
translations = await translation.async_get_translations(
|
||||
|
@ -7,7 +7,7 @@ aiofiles==24.1.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp==3.12.12
|
||||
aiohttp==3.12.13
|
||||
aiohttp_cors==0.7.0
|
||||
aiousbwatcher==1.1.1
|
||||
aiozoneinfo==0.2.3
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.6.1"
|
||||
version = "2025.6.2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@ -29,7 +29,7 @@ dependencies = [
|
||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||
# Lib can be removed with 2025.11
|
||||
"aiohasupervisor==0.3.1",
|
||||
"aiohttp==3.12.12",
|
||||
"aiohttp==3.12.13",
|
||||
"aiohttp_cors==0.7.0",
|
||||
"aiohttp-fast-zlib==0.3.0",
|
||||
"aiohttp-asyncmdnsresolver==0.1.1",
|
||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@ -6,7 +6,7 @@
|
||||
aiodns==3.5.0
|
||||
aiofiles==24.1.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp==3.12.12
|
||||
aiohttp==3.12.13
|
||||
aiohttp_cors==0.7.0
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
|
32
requirements_all.txt
generated
32
requirements_all.txt
generated
@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.1.2
|
||||
aioamazondevices==3.1.14
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==32.2.1
|
||||
aioesphomeapi==33.0.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -265,7 +265,7 @@ aioharmony==0.5.2
|
||||
aiohasupervisor==0.3.1
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.17.1
|
||||
aiohomeconnect==0.18.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.14
|
||||
@ -683,7 +683,7 @@ brunt==1.2.0
|
||||
bt-proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.12.4
|
||||
bthome-ble==3.13.1
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@ -765,7 +765,7 @@ debugpy==1.8.14
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==13.3.0
|
||||
deebot-client==13.4.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -1161,7 +1161,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.74
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.3
|
||||
@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.3
|
||||
home-assistant-intents==2025.6.10
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.5
|
||||
homematicip==2.0.6
|
||||
|
||||
# homeassistant.components.horizon
|
||||
horimote==0.4.1
|
||||
@ -1203,7 +1203,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@ -1448,7 +1448,7 @@ monzopy==1.4.2
|
||||
mopeka-iot-ble==0.8.0
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.27
|
||||
motionblinds==0.6.28
|
||||
|
||||
# homeassistant.components.motionblinds_ble
|
||||
motionblindsble==0.1.3
|
||||
@ -2096,7 +2096,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.8
|
||||
pylamarzocco==2.0.9
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@ -2236,7 +2236,7 @@ pypaperless==4.1.0
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.6
|
||||
pypck==0.8.8
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@ -2341,7 +2341,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==0.8.2
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@ -2652,7 +2652,7 @@ renault-api==0.3.1
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.13.5
|
||||
reolink-aio==0.14.1
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@ -2673,7 +2673,7 @@ rjpl==0.3.6
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.3
|
||||
rokuecp==0.19.5
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.10
|
||||
@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.11.0
|
||||
uiprotect==7.14.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@ -3180,7 +3180,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.59
|
||||
zha==0.0.60
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
32
requirements_test_all.txt
generated
32
requirements_test_all.txt
generated
@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.1.2
|
||||
aioamazondevices==3.1.14
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==32.2.1
|
||||
aioesphomeapi==33.0.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -250,7 +250,7 @@ aioharmony==0.5.2
|
||||
aiohasupervisor==0.3.1
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.17.1
|
||||
aiohomeconnect==0.18.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.14
|
||||
@ -607,7 +607,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.12.4
|
||||
bthome-ble==3.13.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.6
|
||||
@ -665,7 +665,7 @@ debugpy==1.8.14
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==13.3.0
|
||||
deebot-client==13.4.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@ -1007,7 +1007,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.74
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.3
|
||||
@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.3
|
||||
home-assistant-intents==2025.6.10
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.5
|
||||
homematicip==2.0.6
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.20.4
|
||||
@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@ -1237,7 +1237,7 @@ monzopy==1.4.2
|
||||
mopeka-iot-ble==0.8.0
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.27
|
||||
motionblinds==0.6.28
|
||||
|
||||
# homeassistant.components.motionblinds_ble
|
||||
motionblindsble==0.1.3
|
||||
@ -1738,7 +1738,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.8
|
||||
pylamarzocco==2.0.9
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19
|
||||
pypaperless==4.1.0
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.6
|
||||
pypck==0.8.8
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@ -1941,7 +1941,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==0.8.2
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@ -2195,7 +2195,7 @@ renault-api==0.3.1
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.13.5
|
||||
reolink-aio==0.14.1
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.66
|
||||
@ -2204,7 +2204,7 @@ rflink==0.0.66
|
||||
ring-doorbell==0.9.13
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.3
|
||||
rokuecp==0.19.5
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.10
|
||||
@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.11.0
|
||||
uiprotect==7.14.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@ -2621,7 +2621,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.59
|
||||
zha==0.0.60
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.63.0
|
||||
|
@ -99,7 +99,7 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
||||
host_mock.sw_upload_progress.return_value = 100
|
||||
host_mock.manufacturer = "Reolink"
|
||||
host_mock.model = TEST_HOST_MODEL
|
||||
host_mock.item_number = TEST_ITEM_NUMBER
|
||||
host_mock.item_number.return_value = TEST_ITEM_NUMBER
|
||||
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||
host_mock.camera_hardware_version.return_value = "IPC_00001"
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.components.shelly.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
PERCENTAGE,
|
||||
STATE_UNAVAILABLE,
|
||||
@ -40,6 +41,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
MOCK_MAC,
|
||||
init_integration,
|
||||
mock_polling_rpc_update,
|
||||
mock_rest_update,
|
||||
@ -1585,3 +1587,45 @@ async def test_rpc_switch_no_returned_energy_sensor(
|
||||
await init_integration(hass, 3)
|
||||
|
||||
assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None
|
||||
|
||||
|
||||
async def test_block_friendly_name_sleeping_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_block_device: Mock,
|
||||
device_registry: DeviceRegistry,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test friendly name for restored sleeping sensor."""
|
||||
entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True)
|
||||
device = register_device(device_registry, entry)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{MOCK_MAC}-sensor_0-temp",
|
||||
suggested_object_id="test_name_temperature",
|
||||
original_name="Test name temperature",
|
||||
disabled_by=None,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
)
|
||||
|
||||
# Old name, the word "temperature" starts with a lower case letter
|
||||
assert entity.original_name == "Test name temperature"
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(entity.entity_id))
|
||||
|
||||
# New name, the word "temperature" starts with a capital letter
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature"
|
||||
|
||||
# Make device online
|
||||
monkeypatch.setattr(mock_block_device, "initialized", True)
|
||||
mock_block_device.mock_online()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert (state := hass.states.get(entity.entity_id))
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature"
|
||||
|
39
tests/components/switchbot_cloud/test_binary_sensor.py
Normal file
39
tests/components/switchbot_cloud/test_binary_sensor.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Test for the switchbot_cloud binary sensors."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from switchbot_api import Device
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import configure_integration
|
||||
|
||||
|
||||
async def test_unsupported_device_type(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
) -> None:
|
||||
"""Test that unsupported device types do not create sensors."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="unsupported-id-1",
|
||||
deviceName="unsupported-device",
|
||||
deviceType="UnsupportedDevice",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.return_value = {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.BINARY_SENSOR]
|
||||
):
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
# Assert no binary sensor entities were created for unsupported device type
|
||||
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
|
||||
assert len([e for e in entities if e.domain == "binary_sensor"]) == 0
|
@ -65,3 +65,29 @@ async def test_meter_no_coordinator_data(
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
async def test_unsupported_device_type(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
) -> None:
|
||||
"""Test that unsupported device types do not create sensors."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="unsupported-id-1",
|
||||
deviceName="unsupported-device",
|
||||
deviceType="UnsupportedDevice",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.return_value = {}
|
||||
|
||||
with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]):
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
# Assert no sensor entities were created for unsupported device type
|
||||
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
|
||||
assert len([e for e in entities if e.domain == "sensor"]) == 0
|
||||
|
@ -146,8 +146,12 @@ async def test_enter_and_exit(
|
||||
assert len(entity_registry.entities) == 1
|
||||
|
||||
|
||||
async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None:
|
||||
"""Test when additional attributes are present."""
|
||||
async def test_enter_with_attrs_as_query(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
webhook_id,
|
||||
) -> None:
|
||||
"""Test when additional attributes are present URL query."""
|
||||
url = f"/api/webhook/{webhook_id}"
|
||||
data = {
|
||||
"timestamp": 123456789,
|
||||
@ -197,6 +201,45 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None
|
||||
assert state.attributes["altitude"] == 123
|
||||
|
||||
|
||||
async def test_enter_with_attrs_as_payload(
|
||||
hass: HomeAssistant, client, webhook_id
|
||||
) -> None:
|
||||
"""Test when additional attributes are present in JSON payload."""
|
||||
url = f"/api/webhook/{webhook_id}"
|
||||
data = {
|
||||
"location": {
|
||||
"coords": {
|
||||
"heading": "105.32",
|
||||
"latitude": "1.0",
|
||||
"longitude": "1.1",
|
||||
"accuracy": 10.5,
|
||||
"altitude": 102.0,
|
||||
"speed": 100.0,
|
||||
},
|
||||
"extras": {},
|
||||
"manual": True,
|
||||
"is_moving": False,
|
||||
"_": "&id=123&lat=1.0&lon=1.1×tamp=2013-09-17T07:32:51Z&",
|
||||
"odometer": 0,
|
||||
"activity": {"type": "still"},
|
||||
"timestamp": "2013-09-17T07:32:51Z",
|
||||
"battery": {"level": 0.1, "is_charging": False},
|
||||
},
|
||||
"device_id": "123",
|
||||
}
|
||||
|
||||
req = await client.post(url, json=data)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTPStatus.OK
|
||||
state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device_id']}")
|
||||
assert state.state == STATE_NOT_HOME
|
||||
assert state.attributes["gps_accuracy"] == 10.5
|
||||
assert state.attributes["battery_level"] == 10.0
|
||||
assert state.attributes["speed"] == 100.0
|
||||
assert state.attributes["bearing"] == 105.32
|
||||
assert state.attributes["altitude"] == 102.0
|
||||
|
||||
|
||||
async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None:
|
||||
"""Test updating two different devices."""
|
||||
url = f"/api/webhook/{webhook_id}"
|
||||
|
@ -162,6 +162,9 @@ test_response_no_power_boost = {
|
||||
http_404_error = requests.exceptions.HTTPError()
|
||||
http_404_error.response = requests.Response()
|
||||
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
|
||||
http_429_error = requests.exceptions.HTTPError()
|
||||
http_429_error.response = requests.Response()
|
||||
http_429_error.response.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
authorisation_response = {
|
||||
"data": {
|
||||
@ -192,6 +195,24 @@ authorisation_response_unauthorised = {
|
||||
}
|
||||
}
|
||||
|
||||
invalid_reauth_response = {
|
||||
"jwt": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
"user_id": 12345,
|
||||
"ttl": 145656758,
|
||||
"refresh_token_ttl": 145756758,
|
||||
"error": False,
|
||||
"status": 200,
|
||||
}
|
||||
|
||||
http_403_error = requests.exceptions.HTTPError()
|
||||
http_403_error.response = requests.Response()
|
||||
http_403_error.response.status_code = HTTPStatus.FORBIDDEN
|
||||
|
||||
http_404_error = requests.exceptions.HTTPError()
|
||||
http_404_error.response = requests.Response()
|
||||
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
@ -216,6 +237,31 @@ async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_no_eco_mode(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response_no_power_boost,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_select(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, response
|
||||
) -> None:
|
||||
|
@ -1,9 +1,6 @@
|
||||
"""Test the Wallbox config flow."""
|
||||
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
|
||||
import requests_mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.wallbox import config_flow
|
||||
@ -24,23 +21,21 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
from . import (
|
||||
authorisation_response,
|
||||
authorisation_response_unauthorised,
|
||||
http_403_error,
|
||||
http_404_error,
|
||||
setup_integration,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
test_response = json.loads(
|
||||
json.dumps(
|
||||
{
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: "xx",
|
||||
CHARGER_ADDED_ENERGY_KEY: "44.697",
|
||||
CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
|
||||
}
|
||||
)
|
||||
)
|
||||
test_response = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: "xx",
|
||||
CHARGER_ADDED_ENERGY_KEY: "44.697",
|
||||
CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
|
||||
}
|
||||
|
||||
|
||||
async def test_show_set_form(hass: HomeAssistant) -> None:
|
||||
@ -59,17 +54,16 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@ -89,17 +83,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response_unauthorised,
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@ -119,17 +112,16 @@ async def test_form_validate_input(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@ -148,18 +140,16 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response_unauthorised),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@ -183,26 +173,16 @@ async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry)
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json={
|
||||
"jwt": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
"user_id": 12345,
|
||||
"ttl": 145656758,
|
||||
"refresh_token_ttl": 145756758,
|
||||
"error": False,
|
||||
"status": 200,
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response_unauthorised),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
|
@ -1,18 +1,18 @@
|
||||
"""Test Wallbox Init Component."""
|
||||
|
||||
import requests_mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components.wallbox.const import (
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.wallbox.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_403_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
setup_integration_connection_error,
|
||||
setup_integration_no_eco_mode,
|
||||
setup_integration_read_only,
|
||||
test_response,
|
||||
)
|
||||
@ -52,18 +52,16 @@ async def test_wallbox_refresh_failed_connection_error_auth(
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=404,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@ -80,18 +78,68 @@ async def test_wallbox_refresh_failed_invalid_auth(
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=403,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=403,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_http_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with authentication error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with authentication error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@ -108,18 +156,16 @@ async def test_wallbox_refresh_failed_connection_error(
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@ -138,3 +184,15 @@ async def test_wallbox_refresh_failed_read_only(
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_setup_load_entry_no_eco_mode(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox Unload."""
|
||||
|
||||
await setup_integration_no_eco_mode(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
@ -1,15 +1,18 @@
|
||||
"""Test Wallbox Lock component."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
setup_integration_platform_not_ready,
|
||||
setup_integration_read_only,
|
||||
@ -28,18 +31,20 @@ async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) -
|
||||
assert state
|
||||
assert state.state == "unlocked"
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_LOCKED_UNLOCKED_KEY: False},
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_LOCK,
|
||||
@ -66,36 +71,73 @@ async def test_wallbox_lock_class_connection_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_LOCKED_UNLOCKED_KEY: False},
|
||||
status_code=404,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
pytest.raises(ConnectionError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_LOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_LOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
pytest.raises(ConnectionError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_lock_class_authentication_error(
|
||||
|
@ -1,22 +1,26 @@
|
||||
"""Test Wallbox Switch component."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.wallbox import InvalidAuth
|
||||
from homeassistant.components.wallbox.const import (
|
||||
CHARGER_ENERGY_PRICE_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
CHARGER_MAX_ICP_CURRENT_KEY,
|
||||
)
|
||||
from homeassistant.components.wallbox.coordinator import InvalidAuth
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_403_error,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
setup_integration_bidir,
|
||||
setup_integration_platform_not_ready,
|
||||
@ -29,6 +33,14 @@ from .const import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
mock_wallbox = Mock()
|
||||
mock_wallbox.authenticate = Mock(return_value=authorisation_response)
|
||||
mock_wallbox.setEnergyCost = Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1})
|
||||
mock_wallbox.setMaxChargingCurrent = Mock(
|
||||
return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}
|
||||
)
|
||||
mock_wallbox.setIcpMaxCurrent = Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10})
|
||||
|
||||
|
||||
async def test_wallbox_number_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
@ -37,17 +49,16 @@ async def test_wallbox_number_class(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=200,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}),
|
||||
),
|
||||
):
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == 6
|
||||
assert state.attributes["max"] == 25
|
||||
@ -82,19 +93,16 @@ async def test_wallbox_number_energy_class(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_ENERGY_PRICE_KEY: 1.1},
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
@ -113,59 +121,113 @@ async def test_wallbox_number_class_connection_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=404,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_connection_error(
|
||||
async def test_wallbox_number_class_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_ENERGY_PRICE_KEY: 1.1},
|
||||
status_code=404,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
async def test_wallbox_number_class_energy_price_update_failed(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_update_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_auth_error(
|
||||
@ -175,28 +237,26 @@ async def test_wallbox_number_class_energy_price_auth_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_ENERGY_PRICE_KEY: 1.1},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_platform_not_ready(
|
||||
@ -218,19 +278,16 @@ async def test_wallbox_number_class_icp_energy(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_MAX_ICP_CURRENT_KEY: 10},
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
@ -249,28 +306,26 @@ async def test_wallbox_number_class_icp_energy_auth_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
pytest.raises(InvalidAuth),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_MAX_ICP_CURRENT_KEY: 10},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidAuth):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_icp_energy_connection_error(
|
||||
@ -280,25 +335,52 @@ async def test_wallbox_number_class_icp_energy_connection_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/chargers/config/12345",
|
||||
json={CHARGER_MAX_ICP_CURRENT_KEY: 10},
|
||||
status_code=404,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
async def test_wallbox_number_class_icp_energy_too_many_request(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, HomeAssistantError
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration_select,
|
||||
test_response,
|
||||
test_response_eco_mode,
|
||||
@ -109,7 +110,41 @@ async def test_wallbox_select_class_error(
|
||||
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
|
||||
new=Mock(side_effect=error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError, match="Error communicating with Wallbox API"),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID,
|
||||
ATTR_OPTION: mode,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS)
|
||||
async def test_wallbox_select_too_many_requests_error(
|
||||
hass: HomeAssistant,
|
||||
entry: MockConfigEntry,
|
||||
mode,
|
||||
response,
|
||||
mock_authenticate,
|
||||
) -> None:
|
||||
"""Test wallbox select class connection error."""
|
||||
|
||||
await setup_integration_select(hass, entry, response)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
|
@ -1,15 +1,16 @@
|
||||
"""Test Wallbox Lock component."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import authorisation_response, setup_integration
|
||||
from . import authorisation_response, http_404_error, http_429_error, setup_integration
|
||||
from .const import MOCK_SWITCH_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@ -26,18 +27,20 @@ async def test_wallbox_switch_class(
|
||||
assert state
|
||||
assert state.state == "on"
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/v3/chargers/12345/remote-action",
|
||||
json={CHARGER_STATUS_ID_KEY: 193},
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
@ -64,72 +67,52 @@ async def test_wallbox_switch_class_connection_error(
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/v3/chargers/12345/remote-action",
|
||||
json={CHARGER_STATUS_ID_KEY: 193},
|
||||
status_code=404,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
# Test behavior when a connection error occurs
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_switch_class_authentication_error(
|
||||
async def test_wallbox_switch_class_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox switch class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=200,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
# Test behavior when a connection error occurs
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_request.post(
|
||||
"https://api.wall-box.com/v3/chargers/12345/remote-action",
|
||||
json={CHARGER_STATUS_ID_KEY: 193},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
@ -21,7 +21,6 @@ ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
|
||||
VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3"
|
||||
CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4"
|
||||
SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports"
|
||||
LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level"
|
||||
ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any"
|
||||
DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any"
|
||||
NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_detection"
|
||||
|
@ -199,6 +199,12 @@ def climate_heatit_z_trm3_no_value_state_fixture() -> dict[str, Any]:
|
||||
return load_json_object_fixture("climate_heatit_z_trm3_no_value_state.json", DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture(name="ring_keypad_state", scope="package")
|
||||
def ring_keypad_state_fixture() -> dict[str, Any]:
|
||||
"""Load the Ring keypad state fixture data."""
|
||||
return load_json_object_fixture("ring_keypad_state.json", DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture(name="nortek_thermostat_state", scope="package")
|
||||
def nortek_thermostat_state_fixture() -> dict[str, Any]:
|
||||
"""Load the nortek thermostat node state fixture data."""
|
||||
@ -876,6 +882,14 @@ def nortek_thermostat_removed_event_fixture(client) -> Node:
|
||||
return Event("node removed", event_data)
|
||||
|
||||
|
||||
@pytest.fixture(name="ring_keypad")
|
||||
def ring_keypad_fixture(client: MagicMock, ring_keypad_state: NodeDataType) -> Node:
|
||||
"""Mock a Ring keypad node."""
|
||||
node = Node(client, copy.deepcopy(ring_keypad_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="integration")
|
||||
async def integration_fixture(
|
||||
hass: HomeAssistant,
|
||||
|
7543
tests/components/zwave_js/fixtures/ring_keypad_state.json
Normal file
7543
tests/components/zwave_js/fixtures/ring_keypad_state.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -97,8 +97,8 @@
|
||||
'value_id': '52-113-0-Home Security-Cover status',
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'disabled': True,
|
||||
'disabled_by': 'integration',
|
||||
'domain': 'button',
|
||||
'entity_category': 'config',
|
||||
'entity_id': 'button.multisensor_6_idle_home_security_cover_status',
|
||||
@ -120,8 +120,8 @@
|
||||
'value_id': '52-113-0-Home Security-Cover status',
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'disabled_by': None,
|
||||
'disabled': True,
|
||||
'disabled_by': 'integration',
|
||||
'domain': 'button',
|
||||
'entity_category': 'config',
|
||||
'entity_id': 'button.multisensor_6_idle_home_security_motion_sensor_status',
|
||||
|
@ -1,10 +1,13 @@
|
||||
"""Test the Z-Wave JS binary sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
STATE_OFF,
|
||||
@ -15,17 +18,17 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .common import (
|
||||
DISABLED_LEGACY_BINARY_SENSOR,
|
||||
ENABLED_LEGACY_BINARY_SENSOR,
|
||||
LOW_BATTERY_BINARY_SENSOR,
|
||||
NOTIFICATION_MOTION_BINARY_SENSOR,
|
||||
PROPERTY_DOOR_STATUS_BINARY_SENSOR,
|
||||
TAMPER_SENSOR,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -34,21 +37,56 @@ def platforms() -> list[str]:
|
||||
return [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
async def test_low_battery_sensor(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration
|
||||
async def test_battery_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ring_keypad: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test boolean binary sensor of type low battery."""
|
||||
state = hass.states.get(LOW_BATTERY_BINARY_SENSOR)
|
||||
"""Test boolean battery binary sensors."""
|
||||
entity_id = "binary_sensor.keypad_v2_low_battery_level"
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.BATTERY
|
||||
|
||||
entity_entry = entity_registry.async_get(LOW_BATTERY_BINARY_SENSOR)
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
|
||||
disabled_binary_sensor_battery_entities = (
|
||||
"binary_sensor.keypad_v2_battery_is_disconnected",
|
||||
"binary_sensor.keypad_v2_fluid_is_low",
|
||||
"binary_sensor.keypad_v2_overheating",
|
||||
"binary_sensor.keypad_v2_rechargeable",
|
||||
"binary_sensor.keypad_v2_used_as_backup",
|
||||
)
|
||||
|
||||
for entity_id in disabled_binary_sensor_battery_entities:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is None # disabled by default
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for entity_id in disabled_binary_sensor_battery_entities:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_enabled_legacy_sensor(
|
||||
hass: HomeAssistant, ecolink_door_sensor, integration
|
||||
|
@ -1,13 +1,21 @@
|
||||
"""Test the Z-Wave JS button entities."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE
|
||||
from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import ATTR_ENTITY_ID, EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -71,11 +79,32 @@ async def test_ping_entity(
|
||||
|
||||
|
||||
async def test_notification_idle_button(
|
||||
hass: HomeAssistant, client, multisensor_6, integration
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client: MagicMock,
|
||||
multisensor_6: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Notification idle button."""
|
||||
node = multisensor_6
|
||||
state = hass.states.get("button.multisensor_6_idle_home_security_cover_status")
|
||||
entity_id = "button.multisensor_6_idle_home_security_cover_status"
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.CONFIG
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
assert hass.states.get(entity_id) is None # disabled by default
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
entity_id,
|
||||
disabled_by=None,
|
||||
)
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
assert (
|
||||
@ -88,13 +117,13 @@ async def test_notification_idle_button(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{
|
||||
ATTR_ENTITY_ID: "button.multisensor_6_idle_home_security_cover_status",
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command_no_wait.call_args_list) == 1
|
||||
args = client.async_send_command_no_wait.call_args_list[0][0][0]
|
||||
assert client.async_send_command_no_wait.call_count == 1
|
||||
args = client.async_send_command_no_wait.call_args[0][0]
|
||||
assert args["command"] == "node.manually_idle_notification_value"
|
||||
assert args["nodeId"] == node.node_id
|
||||
assert args["valueId"] == {
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Test entity discovery for device-specific schemas for the Z-Wave JS integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
|
||||
from homeassistant.components.number import (
|
||||
@ -12,7 +14,6 @@ from homeassistant.components.number import (
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
@ -26,12 +27,13 @@ from homeassistant.components.zwave_js.discovery import (
|
||||
from homeassistant.components.zwave_js.discovery_data_template import (
|
||||
DynamicCurrentTempClimateDataTemplate,
|
||||
)
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_aeon_smart_switch_6_state(
|
||||
@ -222,17 +224,24 @@ async def test_merten_507801_disabled_enitites(
|
||||
async def test_zooz_zen72(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
switch_zooz_zen72,
|
||||
integration,
|
||||
client: MagicMock,
|
||||
switch_zooz_zen72: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that Zooz ZEN72 Indicators are discovered as number entities."""
|
||||
assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 # includes ping
|
||||
entity_id = "number.z_wave_plus_700_series_dimmer_switch_indicator_value"
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.entity_category == EntityCategory.CONFIG
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category == EntityCategory.CONFIG
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
assert hass.states.get(entity_id) is None # disabled by default
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
client.async_send_command.reset_mock()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
@ -246,7 +255,7 @@ async def test_zooz_zen72(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_count == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == switch_zooz_zen72.node_id
|
||||
@ -260,16 +269,18 @@ async def test_zooz_zen72(
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
entity_id = "button.z_wave_plus_700_series_dimmer_switch_identify"
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.entity_category == EntityCategory.CONFIG
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category == EntityCategory.CONFIG
|
||||
assert entity_entry.disabled_by is None
|
||||
assert hass.states.get(entity_id) is not None
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_count == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == switch_zooz_zen72.node_id
|
||||
@ -285,53 +296,55 @@ async def test_indicator_test(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
indicator_test,
|
||||
integration,
|
||||
client: MagicMock,
|
||||
indicator_test: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that Indicators are discovered properly.
|
||||
|
||||
This test covers indicators that we don't already have device fixtures for.
|
||||
"""
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={get_device_id(client.driver, indicator_test)}
|
||||
binary_sensor_entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor"
|
||||
sensor_entity_id = "sensor.this_is_a_fake_device_sensor"
|
||||
switch_entity_id = "switch.this_is_a_fake_device_switch"
|
||||
|
||||
for entity_id in (
|
||||
binary_sensor_entity_id,
|
||||
sensor_entity_id,
|
||||
):
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
assert hass.states.get(entity_id) is None # disabled by default
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
|
||||
entity_id = switch_entity_id
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category == EntityCategory.CONFIG
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
assert hass.states.get(entity_id) is None # disabled by default
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
assert device
|
||||
entities = er.async_entries_for_device(entity_registry, device.id)
|
||||
await hass.async_block_till_done()
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
def len_domain(domain):
|
||||
return len([entity for entity in entities if entity.domain == domain])
|
||||
|
||||
assert len_domain(NUMBER_DOMAIN) == 0
|
||||
assert len_domain(BUTTON_DOMAIN) == 1 # only ping
|
||||
assert len_domain(BINARY_SENSOR_DOMAIN) == 1
|
||||
assert len_domain(SENSOR_DOMAIN) == 3 # include node status + last seen
|
||||
assert len_domain(SWITCH_DOMAIN) == 1
|
||||
|
||||
entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor"
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
entity_id = binary_sensor_entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
entity_id = "sensor.this_is_a_fake_device_sensor"
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
entity_id = sensor_entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "0.0"
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
entity_id = "switch.this_is_a_fake_device_switch"
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.entity_category == EntityCategory.CONFIG
|
||||
entity_id = switch_entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
@ -342,7 +355,7 @@ async def test_indicator_test(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_count == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == indicator_test.node_id
|
||||
@ -362,7 +375,7 @@ async def test_indicator_test(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_count == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == indicator_test.node_id
|
||||
|
@ -1812,7 +1812,8 @@ async def test_disabled_node_status_entity_on_node_replaced(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_disabled_entity_on_value_removed(
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_remove_entity_on_value_removed(
|
||||
hass: HomeAssistant,
|
||||
zp3111: Node,
|
||||
client: MagicMock,
|
||||
@ -1823,15 +1824,6 @@ async def test_disabled_entity_on_value_removed(
|
||||
"button.4_in_1_sensor_idle_home_security_cover_status"
|
||||
)
|
||||
|
||||
# must reload the integration when enabling an entity
|
||||
await hass.config_entries.async_unload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert integration.state is ConfigEntryState.NOT_LOADED
|
||||
integration.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert integration.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(idle_cover_status_button_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test the Z-Wave JS sensor platform."""
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.const.command_class.meter import MeterType
|
||||
@ -26,6 +27,7 @@ from homeassistant.components.zwave_js.sensor import (
|
||||
CONTROLLER_STATISTICS_KEY_MAP,
|
||||
NODE_STATISTICS_KEY_MAP,
|
||||
)
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
@ -35,6 +37,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UV_INDEX,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@ -45,6 +48,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .common import (
|
||||
AIR_TEMPERATURE_SENSOR,
|
||||
@ -57,7 +61,94 @@ from .common import (
|
||||
VOLTAGE_SENSOR,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
async def test_battery_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ring_keypad: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test numeric battery sensors."""
|
||||
entity_id = "sensor.keypad_v2_battery_level"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "100.0"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY
|
||||
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
|
||||
disabled_sensor_battery_entities = (
|
||||
"sensor.keypad_v2_chargingstatus",
|
||||
"sensor.keypad_v2_maximum_capacity",
|
||||
"sensor.keypad_v2_rechargeorreplace",
|
||||
"sensor.keypad_v2_temperature",
|
||||
)
|
||||
|
||||
for entity_id in disabled_sensor_battery_entities:
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is None # disabled by default
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "sensor.keypad_v2_chargingstatus"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "Maintaining"
|
||||
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
|
||||
assert ATTR_STATE_CLASS not in state.attributes
|
||||
|
||||
entity_id = "sensor.keypad_v2_maximum_capacity"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert (
|
||||
state.state == "0"
|
||||
) # This should be None/unknown but will be fixed in a future PR.
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
|
||||
assert ATTR_DEVICE_CLASS not in state.attributes
|
||||
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
||||
|
||||
entity_id = "sensor.keypad_v2_rechargeorreplace"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "No"
|
||||
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
|
||||
assert ATTR_STATE_CLASS not in state.attributes
|
||||
|
||||
entity_id = "sensor.keypad_v2_temperature"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert (
|
||||
state.state == "0"
|
||||
) # This should be None/unknown but will be fixed in a future PR.
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
|
||||
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
||||
|
||||
|
||||
async def test_numeric_sensor(
|
||||
|
@ -16,6 +16,7 @@ from homeassistant import exceptions
|
||||
from homeassistant.auth.permissions import PolicyPermissions
|
||||
import homeassistant.components # noqa: F401
|
||||
from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group
|
||||
from homeassistant.components.input_button import DOMAIN as DOMAIN_INPUT_BUTTON
|
||||
from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER
|
||||
from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND
|
||||
from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH
|
||||
@ -42,7 +43,12 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
service,
|
||||
)
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import (
|
||||
Integration,
|
||||
async_get_integration,
|
||||
async_get_integrations,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.yaml.loader import parse_yaml
|
||||
|
||||
@ -1092,38 +1098,66 @@ async def test_async_get_all_descriptions_failing_integration(
|
||||
"""Test async_get_all_descriptions when async_get_integrations returns an exception."""
|
||||
group_config = {DOMAIN_GROUP: {}}
|
||||
await async_setup_component(hass, DOMAIN_GROUP, group_config)
|
||||
descriptions = await service.async_get_all_descriptions(hass)
|
||||
|
||||
assert len(descriptions) == 1
|
||||
|
||||
assert "description" in descriptions["group"]["reload"]
|
||||
assert "fields" in descriptions["group"]["reload"]
|
||||
|
||||
logger_config = {DOMAIN_LOGGER: {}}
|
||||
await async_setup_component(hass, DOMAIN_LOGGER, logger_config)
|
||||
|
||||
input_button_config = {DOMAIN_INPUT_BUTTON: {}}
|
||||
await async_setup_component(hass, DOMAIN_INPUT_BUTTON, input_button_config)
|
||||
|
||||
async def wrap_get_integrations(
|
||||
hass: HomeAssistant, domains: Iterable[str]
|
||||
) -> dict[str, Integration | Exception]:
|
||||
integrations = await async_get_integrations(hass, domains)
|
||||
integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml")
|
||||
return integrations
|
||||
|
||||
async def wrap_get_translations(
|
||||
hass: HomeAssistant,
|
||||
language: str,
|
||||
category: str,
|
||||
integrations: Iterable[str] | None = None,
|
||||
config_flow: bool | None = None,
|
||||
) -> dict[str, str]:
|
||||
translations = await async_get_translations(
|
||||
hass, language, category, integrations, config_flow
|
||||
)
|
||||
return {
|
||||
key: value
|
||||
for key, value in translations.items()
|
||||
if not key.startswith("component.logger.services.")
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.helpers.service.async_get_integrations",
|
||||
return_value={"logger": ImportError},
|
||||
wraps=wrap_get_integrations,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.helpers.service.translation.async_get_translations",
|
||||
return_value={},
|
||||
wrap_get_translations,
|
||||
),
|
||||
):
|
||||
descriptions = await service.async_get_all_descriptions(hass)
|
||||
|
||||
assert len(descriptions) == 2
|
||||
assert len(descriptions) == 3
|
||||
assert "Failed to load integration: logger" in caplog.text
|
||||
|
||||
# Services are empty defaults if the load fails but should
|
||||
# not raise
|
||||
assert descriptions[DOMAIN_GROUP]["remove"]["description"]
|
||||
assert descriptions[DOMAIN_GROUP]["remove"]["fields"]
|
||||
|
||||
assert descriptions[DOMAIN_LOGGER]["set_level"] == {
|
||||
"description": "",
|
||||
"fields": {},
|
||||
"name": "",
|
||||
}
|
||||
|
||||
assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["description"]
|
||||
assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {}
|
||||
assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"]
|
||||
|
||||
hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None)
|
||||
service.async_set_service_schema(
|
||||
hass, DOMAIN_LOGGER, "new_service", {"description": "new service"}
|
||||
|
Reference in New Issue
Block a user