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:
Franck Nijhof
2025-06-23 20:37:52 +02:00
committed by GitHub
84 changed files with 9071 additions and 665 deletions

View File

@ -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"

View File

@ -1,5 +1,6 @@
{
"domain": "switchbot",
"name": "SwitchBot",
"integrations": ["switchbot", "switchbot_cloud"]
"integrations": ["switchbot", "switchbot_cloud"],
"iot_standards": ["matter"]
}

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.2"]
"requirements": ["aioamazondevices==3.1.14"]
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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"
],

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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."]
}

View File

@ -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"]
}

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.8"]
"requirements": ["pylamarzocco==2.0.9"]
}

View File

@ -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)

View File

@ -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",

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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)

View File

@ -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 = {

View File

@ -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"]
}

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==10.0.0"]
"requirements": ["ical==10.0.4"]
}

View File

@ -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,

View File

@ -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"]
}

View File

@ -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",

View File

@ -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)}

View File

@ -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

View File

@ -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

View File

@ -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}"

View File

@ -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)

View File

@ -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,
)

View File

@ -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),
)

View File

@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.4"]
"requirements": ["pysmartthings==3.2.5"]
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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,

View File

@ -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 = {

View File

@ -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"

View File

@ -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",

View File

@ -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"

View File

@ -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."""

View File

@ -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(

View File

@ -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(

View File

@ -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):

View File

@ -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):

View File

@ -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"
}
}
}

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.74"]
"requirements": ["holidays==0.75"]
}

View File

@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.59"],
"requirements": ["zha==0.0.60"],
"usb": [
{
"vid": "10C4",

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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)

View File

@ -6426,7 +6426,10 @@
"iot_class": "cloud_polling",
"name": "SwitchBot Cloud"
}
}
},
"iot_standards": [
"matter"
]
},
"switcher_kis": {
"name": "Switcher",

View File

@ -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(

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View 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

View File

@ -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

View File

@ -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&timestamp=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}"

View File

@ -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:

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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,
)

View File

@ -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,

View File

@ -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,
)

View File

@ -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"

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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',

View File

@ -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

View File

@ -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"] == {

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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"}