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
commit 773c25041a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 9071 additions and 665 deletions

View File

@ -37,7 +37,7 @@ on:
type: boolean type: boolean
env: env:
CACHE_VERSION: 2 CACHE_VERSION: 3
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.6" HA_SHORT_VERSION: "2025.6"

View File

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

View File

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

View File

@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome", "documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push", "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", "documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"], "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/#"], "mqtt": ["esphome/discover/#"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"aioesphomeapi==32.2.1", "aioesphomeapi==33.0.0",
"esphome-dashboard-api==1.3.0", "esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0" "bleak-esphome==2.16.0"
], ],

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google", "documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "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, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "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", "documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aiohomeconnect"], "loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.1"], "requirements": ["aiohomeconnect==0.18.0"],
"zeroconf": ["_homeconnect._tcp.local."] "zeroconf": ["_homeconnect._tcp.local."]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["homematicip"], "loggers": ["homematicip"],
"requirements": ["homematicip==2.0.5"] "requirements": ["homematicip==2.0.6"]
} }

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "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] CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER]
).target_temperature ).target_temperature
), ),
available_fn=(
lambda coordinator: WidgetType.CM_COFFEE_BOILER
in coordinator.device.dashboard.config
),
), ),
LaMarzoccoNumberEntityDescription( LaMarzoccoNumberEntityDescription(
key="smart_standby_time", key="smart_standby_time",
@ -221,7 +225,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
entity_description: LaMarzoccoNumberEntityDescription entity_description: LaMarzoccoNumberEntityDescription
@property @property
def native_value(self) -> float: def native_value(self) -> float | int:
"""Return the current value.""" """Return the current value."""
return self.entity_description.native_value_fn(self.coordinator.device) return self.entity_description.native_value_fn(self.coordinator.device)

View File

@ -57,6 +57,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
).ready_start_time ).ready_start_time
), ),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
available_fn=(
lambda coordinator: WidgetType.CM_COFFEE_BOILER
in coordinator.device.dashboard.config
),
), ),
LaMarzoccoSensorEntityDescription( LaMarzoccoSensorEntityDescription(
key="steam_boiler_ready_time", key="steam_boiler_ready_time",

View File

@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn", "documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pypck"], "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", "documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["ical"], "loggers": ["ical"],
"requirements": ["ical==10.0.0"] "requirements": ["ical==10.0.4"]
} }

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo", "documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling", "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 import logging
from typing import Any from typing import Any
import dns.asyncresolver
import dns.rdata import dns.rdata
import dns.rdataclass import dns.rdataclass
import dns.rdatatype import dns.rdatatype
@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def load_dnspython_rdata_classes() -> None: def prevent_dnspython_blocking_operations() -> None:
"""Load dnspython rdata classes used by mcstatus.""" """Prevent dnspython blocking operations by pre-loading required data."""
# Blocking import: https://github.com/rthalley/dnspython/issues/1083
for rdtype in dns.rdatatype.RdataType: for rdtype in dns.rdatatype.RdataType:
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT: 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] 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( async def async_setup_entry(
hass: HomeAssistant, entry: MinecraftServerConfigEntry hass: HomeAssistant, entry: MinecraftServerConfigEntry
) -> bool: ) -> bool:
"""Set up Minecraft Server from a config entry.""" """Set up Minecraft Server from a config entry."""
await hass.async_add_executor_job(prevent_dnspython_blocking_operations)
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
await hass.async_add_executor_job(load_dnspython_rdata_classes)
# Create coordinator instance and store it. # Create coordinator instance and store it.
coordinator = MinecraftServerCoordinator(hass, entry) coordinator = MinecraftServerCoordinator(hass, entry)

View File

@ -62,6 +62,7 @@ TILT_DEVICE_MAP = {
BlindType.VerticalBlind: CoverDeviceClass.BLIND, BlindType.VerticalBlind: CoverDeviceClass.BLIND,
BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND, BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND,
BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, BlindType.VerticalBlindRight: CoverDeviceClass.BLIND,
BlindType.RollerTiltMotor: CoverDeviceClass.BLIND,
} }
TILT_ONLY_DEVICE_MAP = { TILT_ONLY_DEVICE_MAP = {

View File

@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds", "documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["motionblinds"], "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" translation_domain=DOMAIN, translation_key="authentication_failed"
) from err ) from err
except OneDriveException as err: except OneDriveException as err:
_LOGGER.debug("Failed to fetch drive data: %s", err, exc_info=True)
raise UpdateFailed( raise UpdateFailed(
translation_domain=DOMAIN, translation_key="update_failed" translation_domain=DOMAIN, translation_key="update_failed"
) from err ) from err

View File

@ -16,10 +16,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
KEY_ADDRESS,
KEY_DURATION_SECONDS, KEY_DURATION_SECONDS,
KEY_ID, KEY_ID,
KEY_LOCALITY,
KEY_PROGRAM_ID, KEY_PROGRAM_ID,
KEY_PROGRAM_NAME, KEY_PROGRAM_NAME,
KEY_RUN_SUMMARIES, KEY_RUN_SUMMARIES,
@ -65,7 +63,6 @@ class RachioCalendarEntity(
super().__init__(coordinator) super().__init__(coordinator)
self.base_station = base_station self.base_station = base_station
self._event: CalendarEvent | None = None self._event: CalendarEvent | None = None
self._location = coordinator.base_station[KEY_ADDRESS][KEY_LOCALITY]
self._attr_translation_placeholders = { self._attr_translation_placeholders = {
"base": coordinator.base_station[KEY_SERIAL_NUMBER] "base": coordinator.base_station[KEY_SERIAL_NUMBER]
} }
@ -87,7 +84,6 @@ class RachioCalendarEntity(
end=dt_util.as_local(start_time) end=dt_util.as_local(start_time)
+ timedelta(seconds=int(event[KEY_TOTAL_RUN_DURATION])), + timedelta(seconds=int(event[KEY_TOTAL_RUN_DURATION])),
description=valves, description=valves,
location=self._location,
) )
def _handle_upcoming_event(self) -> dict[str, Any] | None: def _handle_upcoming_event(self) -> dict[str, Any] | None:
@ -155,7 +151,6 @@ class RachioCalendarEntity(
start=event_start, start=event_start,
end=event_end, end=event_end,
description=valves, description=valves,
location=self._location,
uid=f"{run[KEY_PROGRAM_ID]}/{run[KEY_START_TIME]}", uid=f"{run[KEY_PROGRAM_ID]}/{run[KEY_START_TIME]}",
) )
event_list.append(event) event_list.append(event)

View File

@ -75,8 +75,6 @@ KEY_PROGRAM_ID = "programId"
KEY_PROGRAM_NAME = "programName" KEY_PROGRAM_NAME = "programName"
KEY_PROGRAM_RUN_SUMMARIES = "valveProgramRunSummaries" KEY_PROGRAM_RUN_SUMMARIES = "valveProgramRunSummaries"
KEY_TOTAL_RUN_DURATION = "totalRunDurationSeconds" KEY_TOTAL_RUN_DURATION = "totalRunDurationSeconds"
KEY_ADDRESS = "address"
KEY_LOCALITY = "locality"
KEY_SKIP = "skip" KEY_SKIP = "skip"
KEY_SKIPPABLE = "skippable" KEY_SKIPPABLE = "skippable"

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["ical"], "loggers": ["ical"],
"quality_scale": "silver", "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)}, connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
name=self._host.api.nvr_name, name=self._host.api.nvr_name,
model=self._host.api.model, model=self._host.api.model,
model_id=self._host.api.item_number, model_id=self._host.api.item_number(),
manufacturer=self._host.api.manufacturer, manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.hardware_version, hw_version=self._host.api.hardware_version,
sw_version=self._host.api.sw_version, sw_version=self._host.api.sw_version,

View File

@ -19,5 +19,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.5"] "requirements": ["reolink-aio==0.14.1"]
} }

View File

@ -10,7 +10,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["rokuecp"], "loggers": ["rokuecp"],
"requirements": ["rokuecp==0.19.3"], "requirements": ["rokuecp==0.19.5"],
"ssdp": [ "ssdp": [
{ {
"st": "roku:ecp", "st": "roku:ecp",

View File

@ -235,11 +235,15 @@ class ShellyButton(ShellyBaseButton):
self._attr_unique_id = f"{coordinator.mac}_{description.key}" self._attr_unique_id = f"{coordinator.mac}_{description.key}"
if isinstance(coordinator, ShellyBlockCoordinator): if isinstance(coordinator, ShellyBlockCoordinator):
self._attr_device_info = get_block_device_info( self._attr_device_info = get_block_device_info(
coordinator.device, coordinator.mac coordinator.device,
coordinator.mac,
suggested_area=coordinator.suggested_area,
) )
else: else:
self._attr_device_info = get_rpc_device_info( 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( self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}

View File

@ -211,7 +211,10 @@ class BlockSleepingClimate(
elif entry is not None: elif entry is not None:
self._unique_id = entry.unique_id self._unique_id = entry.unique_id
self._attr_device_info = get_block_device_info( 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._attr_name = get_block_entity_name(
self.coordinator.device, sensor_block, None self.coordinator.device, sensor_block, None

View File

@ -31,7 +31,11 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback 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.debounce import Debouncer
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -114,6 +118,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
self.device = device self.device = device
self.device_id: str | None = None self.device_id: str | None = None
self._pending_platforms: list[Platform] | 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 device_name = device.name if device.initialized else entry.title
interval_td = timedelta(seconds=update_interval) interval_td = timedelta(seconds=update_interval)
# The device has come online at least once. In the case of a sleeping RPC # 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)}", 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)}", 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 self.device_id = device_entry.id
async def shutdown(self) -> None: async def shutdown(self) -> None:
@ -825,6 +835,15 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
except InvalidAuthError: except InvalidAuthError:
self.config_entry.async_start_reauth(self.hass) self.config_entry.async_start_reauth(self.hass)
return 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: except DeviceConnectionError as err:
# If the device is restarting or has gone offline before # If the device is restarting or has gone offline before
# the ping/pong timeout happens, the shutdown command # the ping/pong timeout happens, the shutdown command

View File

@ -362,7 +362,10 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
self.block = block self.block = block
self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_name = get_block_entity_name(coordinator.device, block)
self._attr_device_info = get_block_device_info( 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}" self._attr_unique_id = f"{coordinator.mac}-{block.description}"
@ -405,7 +408,10 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
super().__init__(coordinator) super().__init__(coordinator)
self.key = key self.key = key
self._attr_device_info = get_rpc_device_info( 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_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, 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_unique_id = f"{coordinator.mac}-{attribute}"
self._attr_device_info = get_block_device_info( 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 self._last_value = None
@ -630,7 +638,10 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
self.entity_description = description self.entity_description = description
self._attr_device_info = get_block_device_info( 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: if block is not None:
@ -642,7 +653,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity):
) )
elif entry is not None: elif entry is not None:
self._attr_unique_id = entry.unique_id self._attr_unique_id = entry.unique_id
self._attr_name = cast(str, entry.original_name)
@callback @callback
def _update_callback(self) -> None: def _update_callback(self) -> None:
@ -698,7 +708,10 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity):
self.entity_description = description self.entity_description = description
self._attr_device_info = get_rpc_device_info( 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 = ( self._attr_unique_id = self._attr_unique_id = (
f"{coordinator.mac}-{key}-{attribute}" f"{coordinator.mac}-{key}-{attribute}"

View File

@ -207,7 +207,10 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
super().__init__(coordinator) super().__init__(coordinator)
self.event_id = int(key.split(":")[-1]) self.event_id = int(key.split(":")[-1])
self._attr_device_info = get_rpc_device_info( 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_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, 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) super().__init__(coordinator, key, attribute, description)
self._attr_device_info = get_rpc_device_info( 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, mac: str,
key: str | None = None, key: str | None = None,
emeter_phase: str | None = None, emeter_phase: str | None = None,
suggested_area: str | None = None,
) -> DeviceInfo: ) -> DeviceInfo:
"""Return device info for RPC device.""" """Return device info for RPC device."""
if key is None: if key is None:
@ -770,6 +771,7 @@ def get_rpc_device_info(
identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")}, identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")},
name=get_rpc_sub_device_name(device, key, emeter_phase), name=get_rpc_sub_device_name(device, key, emeter_phase),
manufacturer="Shelly", manufacturer="Shelly",
suggested_area=suggested_area,
via_device=(DOMAIN, mac), via_device=(DOMAIN, mac),
) )
@ -784,6 +786,7 @@ def get_rpc_device_info(
identifiers={(DOMAIN, f"{mac}-{key}")}, identifiers={(DOMAIN, f"{mac}-{key}")},
name=get_rpc_sub_device_name(device, key), name=get_rpc_sub_device_name(device, key),
manufacturer="Shelly", manufacturer="Shelly",
suggested_area=suggested_area,
via_device=(DOMAIN, mac), via_device=(DOMAIN, mac),
) )
@ -805,7 +808,10 @@ def get_blu_trv_device_info(
def get_block_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: ) -> DeviceInfo:
"""Return device info for Block device.""" """Return device info for Block device."""
if ( if (
@ -820,6 +826,7 @@ def get_block_device_info(
identifiers={(DOMAIN, f"{mac}-{block.description}")}, identifiers={(DOMAIN, f"{mac}-{block.description}")},
name=get_block_sub_device_name(device, block), name=get_block_sub_device_name(device, block),
manufacturer="Shelly", manufacturer="Shelly",
suggested_area=suggested_area,
via_device=(DOMAIN, mac), via_device=(DOMAIN, mac),
) )

View File

@ -30,5 +30,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"quality_scale": "bronze", "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[ for description in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[
device.device_type 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) SwitchBotCloudSensor(data.api, device, coordinator, description)
for device, coordinator in data.devices.sensors for device, coordinator in data.devices.sensors
for description in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type] 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=True,
polling_value_fn=lambda x: x != "<invalid>", polling_value_fn=lambda x: x != "<invalid>",
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( 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, entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,

View File

@ -1,9 +1,12 @@
"""Support for Traccar Client.""" """Support for Traccar Client."""
from http import HTTPStatus from http import HTTPStatus
from json import JSONDecodeError
import logging
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components import webhook from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -20,7 +23,6 @@ from .const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
ATTR_SPEED, ATTR_SPEED,
ATTR_TIMESTAMP,
DOMAIN, DOMAIN,
) )
@ -29,6 +31,7 @@ PLATFORMS = [Platform.DEVICE_TRACKER]
TRACKER_UPDATE = f"{DOMAIN}_tracker_update" TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
LOGGER = logging.getLogger(__name__)
DEFAULT_ACCURACY = 200 DEFAULT_ACCURACY = 200
DEFAULT_BATTERY = -1 DEFAULT_BATTERY = -1
@ -49,21 +52,50 @@ WEBHOOK_SCHEMA = vol.Schema(
vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float),
vol.Optional(ATTR_BEARING): vol.Coerce(float), vol.Optional(ATTR_BEARING): vol.Coerce(float),
vol.Optional(ATTR_SPEED): vol.Coerce(float), vol.Optional(ATTR_SPEED): vol.Coerce(float),
vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int),
}, },
extra=vol.REMOVE_EXTRA, 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( async def handle_webhook(
hass: HomeAssistant, webhook_id: str, request: web.Request hass: HomeAssistant,
webhook_id: str,
request: web.Request,
) -> web.Response: ) -> web.Response:
"""Handle incoming webhook with Traccar Client request.""" """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: try:
data = WEBHOOK_SCHEMA(dict(request.query)) data = WEBHOOK_SCHEMA(requestdata)
except vol.MultipleInvalid as error: except vol.MultipleInvalid as error:
LOGGER.warning(humanize_error(requestdata, error))
return web.Response( return web.Response(
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY text=error.error_message,
status=HTTPStatus.UNPROCESSABLE_ENTITY,
) )
attrs = { attrs = {

View File

@ -17,7 +17,6 @@ ATTR_LONGITUDE = "lon"
ATTR_MOTION = "motion" ATTR_MOTION = "motion"
ATTR_SPEED = "speed" ATTR_SPEED = "speed"
ATTR_STATUS = "status" ATTR_STATUS = "status"
ATTR_TIMESTAMP = "timestamp"
ATTR_TRACKER = "tracker" ATTR_TRACKER = "tracker"
ATTR_TRACCAR_ID = "traccar_id" ATTR_TRACCAR_ID = "traccar_id"

View File

@ -40,7 +40,7 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"], "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": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -3,7 +3,7 @@
from enum import StrEnum from enum import StrEnum
DOMAIN = "wallbox" DOMAIN = "wallbox"
UPDATE_INTERVAL = 30 UPDATE_INTERVAL = 60
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"] BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]
@ -74,3 +74,4 @@ class EcoSmartMode(StrEnum):
OFF = "off" OFF = "off"
ECO_MODE = "eco_mode" ECO_MODE = "eco_mode"
FULL_SOLAR = "full_solar" 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: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
raise ConfigEntryAuthFailed from wallbox_connection_error 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 return require_authentication
@ -137,49 +139,65 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _get_data(self) -> dict[str, Any]: def _get_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component.""" """Get new sensor data for Wallbox component."""
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station) try:
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][ data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
CHARGER_MAX_CHARGING_CURRENT_KEY 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_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] = ( data[CHARGER_CURRENCY_KEY] = (
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
) )
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
) )
# Set current solar charging mode # Set current solar charging mode
eco_smart_enabled = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ eco_smart_enabled = (
CHARGER_ECO_SMART_STATUS_KEY data[CHARGER_DATA_KEY]
] .get(CHARGER_ECO_SMART_KEY, {})
eco_smart_mode = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ .get(CHARGER_ECO_SMART_STATUS_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
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]: async def _async_update_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component.""" """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: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error 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: async def async_set_charging_current(self, charging_current: float) -> None:
"""Set maximum charging current for Wallbox.""" """Set maximum charging current for Wallbox."""
@ -210,7 +234,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error 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: async def async_set_icp_current(self, icp_current: float) -> None:
"""Set maximum icp current for Wallbox.""" """Set maximum icp current for Wallbox."""
@ -220,8 +250,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _set_energy_cost(self, energy_cost: float) -> None: def _set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox.""" """Set energy cost for Wallbox."""
try:
self._wallbox.setEnergyCost(self._station, energy_cost) 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: async def async_set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox.""" """Set energy cost for Wallbox."""
@ -239,7 +277,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error 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: async def async_set_lock_unlock(self, lock: bool) -> None:
"""Set wallbox to locked or unlocked.""" """Set wallbox to locked or unlocked."""
@ -249,11 +293,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _pause_charger(self, pause: bool) -> None: def _pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume.""" """Set wallbox to pause or resume."""
try:
if pause: if pause:
self._wallbox.pauseChargingSession(self._station) self._wallbox.pauseChargingSession(self._station)
else: else:
self._wallbox.resumeChargingSession(self._station) 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: async def async_pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume.""" """Set wallbox to pause or resume."""
@ -263,13 +315,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _set_eco_smart(self, option: str) -> None: def _set_eco_smart(self, option: str) -> None:
"""Set wallbox solar charging mode.""" """Set wallbox solar charging mode."""
try:
if option == EcoSmartMode.ECO_MODE: if option == EcoSmartMode.ECO_MODE:
self._wallbox.enableEcoSmart(self._station, 0) self._wallbox.enableEcoSmart(self._station, 0)
elif option == EcoSmartMode.FULL_SOLAR: elif option == EcoSmartMode.FULL_SOLAR:
self._wallbox.enableEcoSmart(self._station, 1) self._wallbox.enableEcoSmart(self._station, 1)
else: else:
self._wallbox.disableEcoSmart(self._station) 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: async def async_set_eco_smart(self, option: str) -> None:
"""Set wallbox solar charging mode.""" """Set wallbox solar charging mode."""

View File

@ -7,7 +7,7 @@ from typing import Any
from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
@ -41,7 +41,7 @@ async def async_setup_entry(
) )
except InvalidAuth: except InvalidAuth:
return return
except ConnectionError as exc: except HomeAssistantError as exc:
raise PlatformNotReady from exc raise PlatformNotReady from exc
async_add_entities( async_add_entities(

View File

@ -12,7 +12,7 @@ from typing import cast
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
@ -93,7 +93,7 @@ async def async_setup_entry(
) )
except InvalidAuth: except InvalidAuth:
return return
except ConnectionError as exc: except HomeAssistantError as exc:
raise PlatformNotReady from exc raise PlatformNotReady from exc
async_add_entities( async_add_entities(

View File

@ -63,15 +63,15 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Create wallbox select entities in HASS.""" """Create wallbox select entities in HASS."""
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
if coordinator.data[CHARGER_ECO_SMART_KEY] != EcoSmartMode.DISABLED:
async_add_entities( async_add_entities(
WallboxSelect(coordinator, description) WallboxSelect(coordinator, description)
for ent in coordinator.data for ent in coordinator.data
if ( if (
(description := SELECT_TYPES.get(ent)) (description := SELECT_TYPES.get(ent))
and description.supported_fn(coordinator) and description.supported_fn(coordinator)
)
) )
)
class WallboxSelect(WallboxEntity, SelectEntity): class WallboxSelect(WallboxEntity, SelectEntity):

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import logging
from typing import cast from typing import cast
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -49,11 +48,6 @@ from .const import (
from .coordinator import WallboxCoordinator from .coordinator import WallboxCoordinator
from .entity import WallboxEntity from .entity import WallboxEntity
CHARGER_STATION = "station"
UPDATE_INTERVAL = 30
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True) @dataclass(frozen=True)
class WallboxSensorEntityDescription(SensorEntityDescription): class WallboxSensorEntityDescription(SensorEntityDescription):

View File

@ -112,6 +112,9 @@
"exceptions": { "exceptions": {
"api_failed": { "api_failed": {
"message": "Error communicating with Wallbox API" "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", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["holidays==0.74"] "requirements": ["holidays==0.75"]
} }

View File

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

View File

@ -318,12 +318,37 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = {
# Mappings for boolean sensors # Mappings for boolean sensors
BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = { BOOLEAN_SENSOR_MAPPINGS: dict[tuple[int, int | str], BinarySensorEntityDescription] = {
CommandClass.BATTERY: BinarySensorEntityDescription( (CommandClass.BATTERY, "backup"): BinarySensorEntityDescription(
key=str(CommandClass.BATTERY), 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, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, 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 # Entity class attributes
self._attr_name = self.generate_name(include_value_name=True) self._attr_name = self.generate_name(include_value_name=True)
primary_value = self.info.primary_value
if description := BOOLEAN_SENSOR_MAPPINGS.get( if description := BOOLEAN_SENSOR_MAPPINGS.get(
self.info.primary_value.command_class (primary_value.command_class, primary_value.property_)
): ):
self.entity_description = description self.entity_description = description

View File

@ -139,7 +139,10 @@ ATTR_TWIST_ASSIST = "twist_assist"
ADDON_SLUG = "core_zwave_js" ADDON_SLUG = "core_zwave_js"
# Sensor entity description constants # 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_CURRENT = "current"
ENTITY_DESC_KEY_VOLTAGE = "voltage" ENTITY_DESC_KEY_VOLTAGE = "voltage"
ENTITY_DESC_KEY_ENERGY_MEASUREMENT = "energy_measurement" ENTITY_DESC_KEY_ENERGY_MEASUREMENT = "energy_measurement"

View File

@ -896,6 +896,7 @@ DISCOVERY_SCHEMAS = [
writeable=False, writeable=False,
), ),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
), ),
# generic text sensors # generic text sensors
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
@ -912,7 +913,6 @@ DISCOVERY_SCHEMAS = [
hint="numeric_sensor", hint="numeric_sensor",
primary_value=ZWaveValueDiscoverySchema( primary_value=ZWaveValueDiscoverySchema(
command_class={ command_class={
CommandClass.BATTERY,
CommandClass.ENERGY_PRODUCTION, CommandClass.ENERGY_PRODUCTION,
CommandClass.SENSOR_ALARM, CommandClass.SENSOR_ALARM,
CommandClass.SENSOR_MULTILEVEL, CommandClass.SENSOR_MULTILEVEL,
@ -921,6 +921,36 @@ DISCOVERY_SCHEMAS = [
), ),
data_template=NumericSensorDataTemplate(), 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( ZWaveDiscoverySchema(
platform=Platform.SENSOR, platform=Platform.SENSOR,
hint="numeric_sensor", hint="numeric_sensor",
@ -932,6 +962,7 @@ DISCOVERY_SCHEMAS = [
), ),
data_template=NumericSensorDataTemplate(), data_template=NumericSensorDataTemplate(),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
), ),
# Meter sensors for Meter CC # Meter sensors for Meter CC
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
@ -957,6 +988,7 @@ DISCOVERY_SCHEMAS = [
writeable=True, writeable=True,
), ),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
), ),
# button for Indicator CC # button for Indicator CC
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
@ -980,6 +1012,7 @@ DISCOVERY_SCHEMAS = [
writeable=True, writeable=True,
), ),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
), ),
# binary switch # binary switch
# barrier operator signaling states # barrier operator signaling states
@ -1184,6 +1217,7 @@ DISCOVERY_SCHEMAS = [
any_available_states={(0, "idle")}, any_available_states={(0, "idle")},
), ),
allow_multi=True, allow_multi=True,
entity_registry_enabled_default=False,
), ),
# event # event
# stateful = False # stateful = False

View File

@ -133,7 +133,10 @@ from homeassistant.const import (
) )
from .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_CO,
ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CO2,
ENTITY_DESC_KEY_CURRENT, ENTITY_DESC_KEY_CURRENT,
@ -380,8 +383,31 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData: def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData:
"""Resolve helper class data for a discovered value.""" """Resolve helper class data for a discovered value."""
if value.command_class == CommandClass.BATTERY: if value.command_class == CommandClass.BATTERY and value.property_ == "level":
return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) 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: if value.command_class == CommandClass.METER:
try: try:

View File

@ -58,7 +58,10 @@ from .const import (
ATTR_VALUE, ATTR_VALUE,
DATA_CLIENT, DATA_CLIENT,
DOMAIN, 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_CO,
ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CO2,
ENTITY_DESC_KEY_CURRENT, ENTITY_DESC_KEY_CURRENT,
@ -95,17 +98,33 @@ from .migrate import async_migrate_statistics_sensors
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
# These descriptions should include device class. # These descriptions should have a non None unit of measurement.
ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = {
tuple[str, str], SensorEntityDescription (ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE): SensorEntityDescription(
] = { key=ENTITY_DESC_KEY_BATTERY_LEVEL,
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
key=ENTITY_DESC_KEY_BATTERY,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE, 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( (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
key=ENTITY_DESC_KEY_CURRENT, key=ENTITY_DESC_KEY_CURRENT,
device_class=SensorDeviceClass.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_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( ENTITY_DESC_KEY_CO: SensorEntityDescription(
key=ENTITY_DESC_KEY_CO, key=ENTITY_DESC_KEY_CO,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@ -538,7 +563,7 @@ def get_entity_description(
"""Return the entity description for the given data.""" """Return the entity description for the given data."""
data_description_key = data.entity_description_key or "" data_description_key = data.entity_description_key or ""
data_unit = data.unit_of_measurement 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), (data_description_key, data_unit),
ENTITY_DESCRIPTION_KEY_MAP.get( ENTITY_DESCRIPTION_KEY_MAP.get(
data_description_key, data_description_key,
@ -588,6 +613,10 @@ async def async_setup_entry(
entities.append( entities.append(
ZWaveListSensor(config_entry, driver, info, entity_description) 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": elif info.platform_hint == "config_parameter":
entities.append( entities.append(
ZWaveConfigParameterSensor( ZWaveConfigParameterSensor(

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

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

View File

@ -682,9 +682,12 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T
def _load_services_files( def _load_services_files(
hass: HomeAssistant, integrations: Iterable[Integration] hass: HomeAssistant, integrations: Iterable[Integration]
) -> list[JSON_TYPE]: ) -> dict[str, JSON_TYPE]:
"""Load service files for multiple integrations.""" """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 @callback
@ -744,10 +747,9 @@ async def async_get_all_descriptions(
_LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc) _LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc)
if integrations: if integrations:
contents = await hass.async_add_executor_job( loaded = await hass.async_add_executor_job(
_load_services_files, hass, integrations _load_services_files, hass, integrations
) )
loaded = dict(zip(domains_with_missing_services, contents, strict=False))
# Load translations for all service domains # Load translations for all service domains
translations = await translation.async_get_translations( translations = await translation.async_get_translations(

View File

@ -7,7 +7,7 @@ aiofiles==24.1.0
aiohasupervisor==0.3.1 aiohasupervisor==0.3.1
aiohttp-asyncmdnsresolver==0.1.1 aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0 aiohttp-fast-zlib==0.3.0
aiohttp==3.12.12 aiohttp==3.12.13
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiousbwatcher==1.1.1 aiousbwatcher==1.1.1
aiozoneinfo==0.2.3 aiozoneinfo==0.2.3

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.6.1" version = "2025.6.2"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." 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 # change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11 # Lib can be removed with 2025.11
"aiohasupervisor==0.3.1", "aiohasupervisor==0.3.1",
"aiohttp==3.12.12", "aiohttp==3.12.13",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.3.0", "aiohttp-fast-zlib==0.3.0",
"aiohttp-asyncmdnsresolver==0.1.1", "aiohttp-asyncmdnsresolver==0.1.1",

2
requirements.txt generated
View File

@ -6,7 +6,7 @@
aiodns==3.5.0 aiodns==3.5.0
aiofiles==24.1.0 aiofiles==24.1.0
aiohasupervisor==0.3.1 aiohasupervisor==0.3.1
aiohttp==3.12.12 aiohttp==3.12.13
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.3.0 aiohttp-fast-zlib==0.3.0
aiohttp-asyncmdnsresolver==0.1.1 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 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==3.1.2 aioamazondevices==3.1.14
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==32.2.1 aioesphomeapi==33.0.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -265,7 +265,7 @@ aioharmony==0.5.2
aiohasupervisor==0.3.1 aiohasupervisor==0.3.1
# homeassistant.components.home_connect # homeassistant.components.home_connect
aiohomeconnect==0.17.1 aiohomeconnect==0.18.0
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.14 aiohomekit==3.2.14
@ -683,7 +683,7 @@ brunt==1.2.0
bt-proximity==0.2.1 bt-proximity==0.2.1
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==3.12.4 bthome-ble==3.13.1
# homeassistant.components.bt_home_hub_5 # homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1 bthomehub5-devicelist==0.1.1
@ -765,7 +765,7 @@ debugpy==1.8.14
# decora==0.6 # decora==0.6
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==13.3.0 deebot-client==13.4.0
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -1161,7 +1161,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.74 holidays==0.75
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250531.3 home-assistant-frontend==20250531.3
@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.3
home-assistant-intents==2025.6.10 home-assistant-intents==2025.6.10
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==2.0.5 homematicip==2.0.6
# homeassistant.components.horizon # homeassistant.components.horizon
horimote==0.4.1 horimote==0.4.1
@ -1203,7 +1203,7 @@ ibmiotf==0.3.4
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
# homeassistant.components.remote_calendar # homeassistant.components.remote_calendar
ical==10.0.0 ical==10.0.4
# homeassistant.components.caldav # homeassistant.components.caldav
icalendar==6.1.0 icalendar==6.1.0
@ -1448,7 +1448,7 @@ monzopy==1.4.2
mopeka-iot-ble==0.8.0 mopeka-iot-ble==0.8.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.27 motionblinds==0.6.28
# homeassistant.components.motionblinds_ble # homeassistant.components.motionblinds_ble
motionblindsble==0.1.3 motionblindsble==0.1.3
@ -2096,7 +2096,7 @@ pykwb==0.0.8
pylacrosse==0.4 pylacrosse==0.4
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
pylamarzocco==2.0.8 pylamarzocco==2.0.9
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==5.1.0 pylast==5.1.0
@ -2236,7 +2236,7 @@ pypaperless==4.1.0
pypca==0.0.7 pypca==0.0.7
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.8.6 pypck==0.8.8
# homeassistant.components.pglab # homeassistant.components.pglab
pypglab==0.0.5 pypglab==0.0.5
@ -2341,7 +2341,7 @@ pysmappee==0.2.29
pysmarlaapi==0.8.2 pysmarlaapi==0.8.2
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.4 pysmartthings==3.2.5
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2652,7 +2652,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.13.5 reolink-aio==0.14.1
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2673,7 +2673,7 @@ rjpl==0.3.6
rocketchat-API==0.6.1 rocketchat-API==0.6.1
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.19.3 rokuecp==0.19.5
# homeassistant.components.romy # homeassistant.components.romy
romy==0.0.10 romy==0.0.10
@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
uiprotect==7.11.0 uiprotect==7.14.1
# homeassistant.components.landisgyr_heat_meter # homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7 ultraheat-api==0.5.7
@ -3180,7 +3180,7 @@ zeroconf==0.147.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.59 zha==0.0.60
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13 zhong-hong-hvac==1.0.13

View File

@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
aioairzone==1.0.0 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==3.1.2 aioamazondevices==3.1.14
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==32.2.1 aioesphomeapi==33.0.0
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -250,7 +250,7 @@ aioharmony==0.5.2
aiohasupervisor==0.3.1 aiohasupervisor==0.3.1
# homeassistant.components.home_connect # homeassistant.components.home_connect
aiohomeconnect==0.17.1 aiohomeconnect==0.18.0
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.14 aiohomekit==3.2.14
@ -607,7 +607,7 @@ brottsplatskartan==1.0.5
brunt==1.2.0 brunt==1.2.0
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==3.12.4 bthome-ble==3.13.1
# homeassistant.components.buienradar # homeassistant.components.buienradar
buienradar==1.0.6 buienradar==1.0.6
@ -665,7 +665,7 @@ debugpy==1.8.14
# decora==0.6 # decora==0.6
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==13.3.0 deebot-client==13.4.0
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -1007,7 +1007,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.74 holidays==0.75
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250531.3 home-assistant-frontend==20250531.3
@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.3
home-assistant-intents==2025.6.10 home-assistant-intents==2025.6.10
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==2.0.5 homematicip==2.0.6
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
httplib2==0.20.4 httplib2==0.20.4
@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
# homeassistant.components.remote_calendar # homeassistant.components.remote_calendar
ical==10.0.0 ical==10.0.4
# homeassistant.components.caldav # homeassistant.components.caldav
icalendar==6.1.0 icalendar==6.1.0
@ -1237,7 +1237,7 @@ monzopy==1.4.2
mopeka-iot-ble==0.8.0 mopeka-iot-ble==0.8.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.27 motionblinds==0.6.28
# homeassistant.components.motionblinds_ble # homeassistant.components.motionblinds_ble
motionblindsble==0.1.3 motionblindsble==0.1.3
@ -1738,7 +1738,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8 pykulersky==0.5.8
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
pylamarzocco==2.0.8 pylamarzocco==2.0.9
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==5.1.0 pylast==5.1.0
@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19
pypaperless==4.1.0 pypaperless==4.1.0
# homeassistant.components.lcn # homeassistant.components.lcn
pypck==0.8.6 pypck==0.8.8
# homeassistant.components.pglab # homeassistant.components.pglab
pypglab==0.0.5 pypglab==0.0.5
@ -1941,7 +1941,7 @@ pysmappee==0.2.29
pysmarlaapi==0.8.2 pysmarlaapi==0.8.2
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.4 pysmartthings==3.2.5
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2195,7 +2195,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.13.5 reolink-aio==0.14.1
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.66 rflink==0.0.66
@ -2204,7 +2204,7 @@ rflink==0.0.66
ring-doorbell==0.9.13 ring-doorbell==0.9.13
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.19.3 rokuecp==0.19.5
# homeassistant.components.romy # homeassistant.components.romy
romy==0.0.10 romy==0.0.10
@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
uiprotect==7.11.0 uiprotect==7.14.1
# homeassistant.components.landisgyr_heat_meter # homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7 ultraheat-api==0.5.7
@ -2621,7 +2621,7 @@ zeroconf==0.147.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.59 zha==0.0.60
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.63.0 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.sw_upload_progress.return_value = 100
host_mock.manufacturer = "Reolink" host_mock.manufacturer = "Reolink"
host_mock.model = TEST_HOST_MODEL 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_model.return_value = TEST_CAM_MODEL
host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_name.return_value = TEST_NVR_NAME
host_mock.camera_hardware_version.return_value = "IPC_00001" 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 ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE, PERCENTAGE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
@ -40,6 +41,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import ( from . import (
MOCK_MAC,
init_integration, init_integration,
mock_polling_rpc_update, mock_polling_rpc_update,
mock_rest_update, mock_rest_update,
@ -1585,3 +1587,45 @@ async def test_rpc_switch_no_returned_energy_sensor(
await init_integration(hass, 3) await init_integration(hass, 3)
assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None 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) entry = await configure_integration(hass)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) 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 assert len(entity_registry.entities) == 1
async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None: async def test_enter_with_attrs_as_query(
"""Test when additional attributes are present.""" hass: HomeAssistant,
client,
webhook_id,
) -> None:
"""Test when additional attributes are present URL query."""
url = f"/api/webhook/{webhook_id}" url = f"/api/webhook/{webhook_id}"
data = { data = {
"timestamp": 123456789, "timestamp": 123456789,
@ -197,6 +201,45 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None
assert state.attributes["altitude"] == 123 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: async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None:
"""Test updating two different devices.""" """Test updating two different devices."""
url = f"/api/webhook/{webhook_id}" 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 = requests.exceptions.HTTPError()
http_404_error.response = requests.Response() http_404_error.response = requests.Response()
http_404_error.response.status_code = HTTPStatus.NOT_FOUND 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 = { authorisation_response = {
"data": { "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: async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
"""Test wallbox sensor class setup.""" """Test wallbox sensor class setup."""
@ -216,6 +237,31 @@ async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None
await hass.async_block_till_done() 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( async def setup_integration_select(
hass: HomeAssistant, entry: MockConfigEntry, response hass: HomeAssistant, entry: MockConfigEntry, response
) -> None: ) -> None:

View File

@ -1,9 +1,6 @@
"""Test the Wallbox config flow.""" """Test the Wallbox config flow."""
from http import HTTPStatus from unittest.mock import Mock, patch
import json
import requests_mock
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.wallbox import config_flow from homeassistant.components.wallbox import config_flow
@ -24,23 +21,21 @@ from homeassistant.data_entry_flow import FlowResultType
from . import ( from . import (
authorisation_response, authorisation_response,
authorisation_response_unauthorised, authorisation_response_unauthorised,
http_403_error,
http_404_error,
setup_integration, setup_integration,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
test_response = json.loads( test_response = {
json.dumps( CHARGER_CHARGING_POWER_KEY: 0,
{ CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
CHARGER_CHARGING_POWER_KEY: 0, CHARGER_CHARGING_SPEED_KEY: 0,
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx", CHARGER_ADDED_RANGE_KEY: "xx",
CHARGER_CHARGING_SPEED_KEY: 0, CHARGER_ADDED_ENERGY_KEY: "44.697",
CHARGER_ADDED_RANGE_KEY: "xx", CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
CHARGER_ADDED_ENERGY_KEY: "44.697", }
CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
}
)
)
async def test_show_set_form(hass: HomeAssistant) -> None: 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_403_error),
status_code=HTTPStatus.FORBIDDEN, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_403_error),
json=test_response, ),
status_code=HTTPStatus.FORBIDDEN, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@ -89,17 +83,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response_unauthorised, new=Mock(side_effect=http_404_error),
status_code=HTTPStatus.NOT_FOUND, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_404_error),
json=test_response, ),
status_code=HTTPStatus.NOT_FOUND, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@ -119,17 +112,16 @@ async def test_form_validate_input(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=HTTPStatus.OK, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=HTTPStatus.OK, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@ -148,18 +140,16 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None:
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response_unauthorised),
status_code=200, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=200, ):
)
result = await entry.start_reauth_flow(hass) result = await entry.start_reauth_flow(hass)
result2 = await hass.config_entries.flow.async_configure( 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) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json={ new=Mock(return_value=authorisation_response_unauthorised),
"jwt": "fakekeyhere", ),
"refresh_token": "refresh_fakekeyhere", patch(
"user_id": 12345, "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"ttl": 145656758, new=Mock(return_value=test_response),
"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,
)
result = await entry.start_reauth_flow(hass) result = await entry.start_reauth_flow(hass)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(

View File

@ -1,18 +1,18 @@
"""Test Wallbox Init Component.""" """Test Wallbox Init Component."""
import requests_mock from unittest.mock import Mock, patch
from homeassistant.components.wallbox.const import ( from homeassistant.components.wallbox.const import DOMAIN
CHARGER_MAX_CHARGING_CURRENT_KEY,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import ( from . import (
authorisation_response, authorisation_response,
http_403_error,
http_429_error,
setup_integration, setup_integration,
setup_integration_connection_error, setup_integration_connection_error,
setup_integration_no_eco_mode,
setup_integration_read_only, setup_integration_read_only,
test_response, test_response,
) )
@ -52,18 +52,16 @@ async def test_wallbox_refresh_failed_connection_error_auth(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_429_error),
status_code=404, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=200, ):
)
wallbox = hass.data[DOMAIN][entry.entry_id] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() await wallbox.async_refresh()
@ -80,18 +78,68 @@ async def test_wallbox_refresh_failed_invalid_auth(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_403_error),
status_code=403, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=http_403_error),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=403, ):
) 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] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() await wallbox.async_refresh()
@ -108,18 +156,16 @@ async def test_wallbox_refresh_failed_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_403_error),
json=test_response, ),
status_code=403, ):
)
wallbox = hass.data[DOMAIN][entry.entry_id] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() 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 await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is ConfigEntryState.NOT_LOADED 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.""" """Test Wallbox Lock component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import ( from . import (
authorisation_response, authorisation_response,
http_429_error,
setup_integration, setup_integration,
setup_integration_platform_not_ready, setup_integration_platform_not_ready,
setup_integration_read_only, setup_integration_read_only,
@ -28,18 +31,20 @@ async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) -
assert state assert state
assert state.state == "unlocked" assert state.state == "unlocked"
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.lockCharger",
"https://api.wall-box.com/v2/charger/12345", new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
json={CHARGER_LOCKED_UNLOCKED_KEY: False}, ),
status_code=200, patch(
) "homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
),
):
await hass.services.async_call( await hass.services.async_call(
"lock", "lock",
SERVICE_LOCK, SERVICE_LOCK,
@ -66,36 +71,73 @@ async def test_wallbox_lock_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.lockCharger",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=ConnectionError),
json={CHARGER_LOCKED_UNLOCKED_KEY: False}, ),
status_code=404, pytest.raises(ConnectionError),
):
await hass.services.async_call(
"lock",
SERVICE_LOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
) )
with pytest.raises(ConnectionError): with (
await hass.services.async_call( patch(
"lock", "homeassistant.components.wallbox.Wallbox.authenticate",
SERVICE_LOCK, new=Mock(return_value=authorisation_response),
{ ),
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, patch(
}, "homeassistant.components.wallbox.Wallbox.lockCharger",
blocking=True, new=Mock(side_effect=ConnectionError),
) ),
with pytest.raises(ConnectionError): patch(
await hass.services.async_call( "homeassistant.components.wallbox.Wallbox.unlockCharger",
"lock", new=Mock(side_effect=ConnectionError),
SERVICE_UNLOCK, ),
{ pytest.raises(ConnectionError),
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, ):
}, await hass.services.async_call(
blocking=True, "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( async def test_wallbox_lock_class_authentication_error(

View File

@ -1,22 +1,26 @@
"""Test Wallbox Switch component.""" """Test Wallbox Switch component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.wallbox import InvalidAuth
from homeassistant.components.wallbox.const import ( from homeassistant.components.wallbox.const import (
CHARGER_ENERGY_PRICE_KEY, CHARGER_ENERGY_PRICE_KEY,
CHARGER_MAX_CHARGING_CURRENT_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY,
CHARGER_MAX_ICP_CURRENT_KEY, CHARGER_MAX_ICP_CURRENT_KEY,
) )
from homeassistant.components.wallbox.coordinator import InvalidAuth
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import HomeAssistantError
from . import ( from . import (
authorisation_response, authorisation_response,
http_403_error,
http_404_error,
http_429_error,
setup_integration, setup_integration,
setup_integration_bidir, setup_integration_bidir,
setup_integration_platform_not_ready, setup_integration_platform_not_ready,
@ -29,6 +33,14 @@ from .const import (
from tests.common import MockConfigEntry 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( async def test_wallbox_number_class(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
@ -37,17 +49,16 @@ async def test_wallbox_number_class(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/v2/charger/12345", new=Mock(return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=200, ):
)
state = hass.states.get(MOCK_NUMBER_ENTITY_ID) state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
assert state.attributes["min"] == 6 assert state.attributes["min"] == 6
assert state.attributes["max"] == 25 assert state.attributes["max"] == 25
@ -82,19 +93,16 @@ async def test_wallbox_number_energy_class(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
mock_request.post( new=Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}),
"https://api.wall-box.com/chargers/config/12345", ),
json={CHARGER_ENERGY_PRICE_KEY: 1.1}, ):
status_code=200,
)
await hass.services.async_call( await hass.services.async_call(
"number", "number",
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@ -113,59 +121,113 @@ async def test_wallbox_number_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=http_404_error),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=404, 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_too_many_requests(
async def test_wallbox_number_class_energy_price_connection_error(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
) -> None: ) -> None:
"""Test wallbox sensor class.""" """Test wallbox sensor class."""
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/chargers/config/12345", new=Mock(side_effect=http_429_error),
json={CHARGER_ENERGY_PRICE_KEY: 1.1}, ),
status_code=404, 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( async def test_wallbox_number_class_energy_price_update_failed(
"number", hass: HomeAssistant, entry: MockConfigEntry
SERVICE_SET_VALUE, ) -> None:
{ """Test wallbox sensor class."""
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1, await setup_integration(hass, entry)
},
blocking=True, 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( 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) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
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( 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) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
mock_request.post( new=Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}),
"https://api.wall-box.com/chargers/config/12345", ),
json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, ):
status_code=200,
)
await hass.services.async_call( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@ -249,28 +306,26 @@ async def test_wallbox_number_class_icp_energy_auth_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
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( 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) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
"https://api.wall-box.com/chargers/config/12345", new=Mock(side_effect=http_404_error),
json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, ),
status_code=404, 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( async def test_wallbox_number_class_icp_energy_too_many_request(
NUMBER_DOMAIN, hass: HomeAssistant, entry: MockConfigEntry
SERVICE_SET_VALUE, ) -> None:
{ """Test wallbox sensor class."""
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10, await setup_integration(hass, entry)
},
blocking=True, 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 ( from . import (
authorisation_response, authorisation_response,
http_404_error, http_404_error,
http_429_error,
setup_integration_select, setup_integration_select,
test_response, test_response,
test_response_eco_mode, test_response_eco_mode,
@ -109,7 +110,41 @@ async def test_wallbox_select_class_error(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart", "homeassistant.components.wallbox.Wallbox.enableEcoSmart",
new=Mock(side_effect=error), 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( await hass.services.async_call(
SELECT_DOMAIN, SELECT_DOMAIN,

View File

@ -1,15 +1,16 @@
"""Test Wallbox Lock component.""" """Test Wallbox Lock component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant 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 .const import MOCK_SWITCH_ENTITY_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -26,18 +27,20 @@ async def test_wallbox_switch_class(
assert state assert state
assert state.state == "on" assert state.state == "on"
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/v3/chargers/12345/remote-action", new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
json={CHARGER_STATUS_ID_KEY: 193}, ),
status_code=200, patch(
) "homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
):
await hass.services.async_call( await hass.services.async_call(
"switch", "switch",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -64,72 +67,52 @@ async def test_wallbox_switch_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.resumeChargingSession",
"https://api.wall-box.com/v3/chargers/12345/remote-action", new=Mock(side_effect=http_404_error),
json={CHARGER_STATUS_ID_KEY: 193}, ),
status_code=404, 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_too_many_requests(
async def test_wallbox_switch_class_authentication_error(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
) -> None: ) -> None:
"""Test wallbox switch class connection error.""" """Test wallbox switch class connection error."""
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
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" 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" CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4"
SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" 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" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any"
DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any"
NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_detection" 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) 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") @pytest.fixture(name="nortek_thermostat_state", scope="package")
def nortek_thermostat_state_fixture() -> dict[str, Any]: def nortek_thermostat_state_fixture() -> dict[str, Any]:
"""Load the nortek thermostat node state fixture data.""" """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) 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") @pytest.fixture(name="integration")
async def integration_fixture( async def integration_fixture(
hass: HomeAssistant, 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', 'value_id': '52-113-0-Home Security-Cover status',
}), }),
dict({ dict({
'disabled': False, 'disabled': True,
'disabled_by': None, 'disabled_by': 'integration',
'domain': 'button', 'domain': 'button',
'entity_category': 'config', 'entity_category': 'config',
'entity_id': 'button.multisensor_6_idle_home_security_cover_status', 'entity_id': 'button.multisensor_6_idle_home_security_cover_status',
@ -120,8 +120,8 @@
'value_id': '52-113-0-Home Security-Cover status', 'value_id': '52-113-0-Home Security-Cover status',
}), }),
dict({ dict({
'disabled': False, 'disabled': True,
'disabled_by': None, 'disabled_by': 'integration',
'domain': 'button', 'domain': 'button',
'entity_category': 'config', 'entity_category': 'config',
'entity_id': 'button.multisensor_6_idle_home_security_motion_sensor_status', '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.""" """Test the Z-Wave JS binary sensor platform."""
from datetime import timedelta
import pytest import pytest
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
STATE_OFF, STATE_OFF,
@ -15,17 +18,17 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from .common import ( from .common import (
DISABLED_LEGACY_BINARY_SENSOR, DISABLED_LEGACY_BINARY_SENSOR,
ENABLED_LEGACY_BINARY_SENSOR, ENABLED_LEGACY_BINARY_SENSOR,
LOW_BATTERY_BINARY_SENSOR,
NOTIFICATION_MOTION_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR,
PROPERTY_DOOR_STATUS_BINARY_SENSOR, PROPERTY_DOOR_STATUS_BINARY_SENSOR,
TAMPER_SENSOR, TAMPER_SENSOR,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture @pytest.fixture
@ -34,21 +37,56 @@ def platforms() -> list[str]:
return [Platform.BINARY_SENSOR] return [Platform.BINARY_SENSOR]
async def test_low_battery_sensor( async def test_battery_sensors(
hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ring_keypad: Node,
integration: MockConfigEntry,
) -> None: ) -> None:
"""Test boolean binary sensor of type low battery.""" """Test boolean battery binary sensors."""
state = hass.states.get(LOW_BATTERY_BINARY_SENSOR) entity_id = "binary_sensor.keypad_v2_low_battery_level"
state = hass.states.get(entity_id)
assert state assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.BATTERY 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
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC 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( async def test_enabled_legacy_sensor(
hass: HomeAssistant, ecolink_door_sensor, integration hass: HomeAssistant, ecolink_door_sensor, integration

View File

@ -1,13 +1,21 @@
"""Test the Z-Wave JS button entities.""" """Test the Z-Wave JS button entities."""
from datetime import timedelta
from unittest.mock import MagicMock
import pytest import pytest
from zwave_js_server.model.node import Node
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS 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.const import DOMAIN, SERVICE_REFRESH_VALUE
from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 @pytest.fixture
@ -71,11 +79,32 @@ async def test_ping_entity(
async def test_notification_idle_button( 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: ) -> None:
"""Test Notification idle button.""" """Test Notification idle button."""
node = multisensor_6 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
assert state.state == "unknown" assert state.state == "unknown"
assert ( assert (
@ -88,13 +117,13 @@ async def test_notification_idle_button(
BUTTON_DOMAIN, BUTTON_DOMAIN,
SERVICE_PRESS, SERVICE_PRESS,
{ {
ATTR_ENTITY_ID: "button.multisensor_6_idle_home_security_cover_status", ATTR_ENTITY_ID: entity_id,
}, },
blocking=True, blocking=True,
) )
assert len(client.async_send_command_no_wait.call_args_list) == 1 assert client.async_send_command_no_wait.call_count == 1
args = client.async_send_command_no_wait.call_args_list[0][0][0] args = client.async_send_command_no_wait.call_args[0][0]
assert args["command"] == "node.manually_idle_notification_value" assert args["command"] == "node.manually_idle_notification_value"
assert args["nodeId"] == node.node_id assert args["nodeId"] == node.node_id
assert args["valueId"] == { assert args["valueId"] == {

View File

@ -1,10 +1,12 @@
"""Test entity discovery for device-specific schemas for the Z-Wave JS integration.""" """Test entity discovery for device-specific schemas for the Z-Wave JS integration."""
from datetime import timedelta
from unittest.mock import MagicMock
import pytest import pytest
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.model.node import Node 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.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
from homeassistant.components.number import ( from homeassistant.components.number import (
@ -12,7 +14,6 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN, DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
) )
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import ( from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN, DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@ -26,12 +27,13 @@ from homeassistant.components.zwave_js.discovery import (
from homeassistant.components.zwave_js.discovery_data_template import ( from homeassistant.components.zwave_js.discovery_data_template import (
DynamicCurrentTempClimateDataTemplate, 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.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er 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( async def test_aeon_smart_switch_6_state(
@ -222,17 +224,24 @@ async def test_merten_507801_disabled_enitites(
async def test_zooz_zen72( async def test_zooz_zen72(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
client, client: MagicMock,
switch_zooz_zen72, switch_zooz_zen72: Node,
integration, integration: MockConfigEntry,
) -> None: ) -> None:
"""Test that Zooz ZEN72 Indicators are discovered as number entities.""" """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" entity_id = "number.z_wave_plus_700_series_dimmer_switch_indicator_value"
entry = entity_registry.async_get(entity_id) entity_entry = entity_registry.async_get(entity_id)
assert entry assert entity_entry
assert entry.entity_category == EntityCategory.CONFIG 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) state = hass.states.get(entity_id)
assert state assert state
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
@ -246,7 +255,7 @@ async def test_zooz_zen72(
}, },
blocking=True, 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] args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value" assert args["command"] == "node.set_value"
assert args["nodeId"] == switch_zooz_zen72.node_id assert args["nodeId"] == switch_zooz_zen72.node_id
@ -260,16 +269,18 @@ async def test_zooz_zen72(
client.async_send_command.reset_mock() client.async_send_command.reset_mock()
entity_id = "button.z_wave_plus_700_series_dimmer_switch_identify" entity_id = "button.z_wave_plus_700_series_dimmer_switch_identify"
entry = entity_registry.async_get(entity_id) entity_entry = entity_registry.async_get(entity_id)
assert entry assert entity_entry
assert entry.entity_category == EntityCategory.CONFIG 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( await hass.services.async_call(
BUTTON_DOMAIN, BUTTON_DOMAIN,
SERVICE_PRESS, SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id}, {ATTR_ENTITY_ID: entity_id},
blocking=True, 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] args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value" assert args["command"] == "node.set_value"
assert args["nodeId"] == switch_zooz_zen72.node_id assert args["nodeId"] == switch_zooz_zen72.node_id
@ -285,53 +296,55 @@ async def test_indicator_test(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
client, client: MagicMock,
indicator_test, indicator_test: Node,
integration, integration: MockConfigEntry,
) -> None: ) -> None:
"""Test that Indicators are discovered properly. """Test that Indicators are discovered properly.
This test covers indicators that we don't already have device fixtures for. This test covers indicators that we don't already have device fixtures for.
""" """
device = device_registry.async_get_device( binary_sensor_entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor"
identifiers={get_device_id(client.driver, indicator_test)} 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 await hass.async_block_till_done()
entities = er.async_entries_for_device(entity_registry, device.id) client.async_send_command.reset_mock()
def len_domain(domain): entity_id = binary_sensor_entity_id
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
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
client.async_send_command.reset_mock() entity_id = sensor_entity_id
entity_id = "sensor.this_is_a_fake_device_sensor"
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "0.0" assert state.state == "0.0"
client.async_send_command.reset_mock() entity_id = switch_entity_id
entity_id = "switch.this_is_a_fake_device_switch"
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.CONFIG
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
@ -342,7 +355,7 @@ async def test_indicator_test(
{ATTR_ENTITY_ID: entity_id}, {ATTR_ENTITY_ID: entity_id},
blocking=True, 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] args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value" assert args["command"] == "node.set_value"
assert args["nodeId"] == indicator_test.node_id assert args["nodeId"] == indicator_test.node_id
@ -362,7 +375,7 @@ async def test_indicator_test(
{ATTR_ENTITY_ID: entity_id}, {ATTR_ENTITY_ID: entity_id},
blocking=True, 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] args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value" assert args["command"] == "node.set_value"
assert args["nodeId"] == indicator_test.node_id 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 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, hass: HomeAssistant,
zp3111: Node, zp3111: Node,
client: MagicMock, client: MagicMock,
@ -1823,15 +1824,6 @@ async def test_disabled_entity_on_value_removed(
"button.4_in_1_sensor_idle_home_security_cover_status" "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) state = hass.states.get(idle_cover_status_button_entity)
assert state assert state
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE

View File

@ -1,6 +1,7 @@
"""Test the Z-Wave JS sensor platform.""" """Test the Z-Wave JS sensor platform."""
import copy import copy
from datetime import timedelta
import pytest import pytest
from zwave_js_server.const.command_class.meter import MeterType 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, CONTROLLER_STATISTICS_KEY_MAP,
NODE_STATISTICS_KEY_MAP, NODE_STATISTICS_KEY_MAP,
) )
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -35,6 +37,7 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
UV_INDEX, UV_INDEX,
EntityCategory, EntityCategory,
Platform,
UnitOfElectricCurrent, UnitOfElectricCurrent,
UnitOfElectricPotential, UnitOfElectricPotential,
UnitOfEnergy, UnitOfEnergy,
@ -45,6 +48,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from .common import ( from .common import (
AIR_TEMPERATURE_SENSOR, AIR_TEMPERATURE_SENSOR,
@ -57,7 +61,94 @@ from .common import (
VOLTAGE_SENSOR, 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( async def test_numeric_sensor(

View File

@ -16,6 +16,7 @@ from homeassistant import exceptions
from homeassistant.auth.permissions import PolicyPermissions from homeassistant.auth.permissions import PolicyPermissions
import homeassistant.components # noqa: F401 import homeassistant.components # noqa: F401
from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group 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.logger import DOMAIN as DOMAIN_LOGGER
from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND
from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH
@ -42,7 +43,12 @@ from homeassistant.helpers import (
entity_registry as er, entity_registry as er,
service, 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.setup import async_setup_component
from homeassistant.util.yaml.loader import parse_yaml 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.""" """Test async_get_all_descriptions when async_get_integrations returns an exception."""
group_config = {DOMAIN_GROUP: {}} group_config = {DOMAIN_GROUP: {}}
await async_setup_component(hass, DOMAIN_GROUP, group_config) 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: {}} logger_config = {DOMAIN_LOGGER: {}}
await async_setup_component(hass, DOMAIN_LOGGER, logger_config) 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 ( with (
patch( patch(
"homeassistant.helpers.service.async_get_integrations", "homeassistant.helpers.service.async_get_integrations",
return_value={"logger": ImportError}, wraps=wrap_get_integrations,
), ),
patch( patch(
"homeassistant.helpers.service.translation.async_get_translations", "homeassistant.helpers.service.translation.async_get_translations",
return_value={}, wrap_get_translations,
), ),
): ):
descriptions = await service.async_get_all_descriptions(hass) 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 assert "Failed to load integration: logger" in caplog.text
# Services are empty defaults if the load fails but should # Services are empty defaults if the load fails but should
# not raise # not raise
assert descriptions[DOMAIN_GROUP]["remove"]["description"]
assert descriptions[DOMAIN_GROUP]["remove"]["fields"]
assert descriptions[DOMAIN_LOGGER]["set_level"] == { assert descriptions[DOMAIN_LOGGER]["set_level"] == {
"description": "", "description": "",
"fields": {}, "fields": {},
"name": "", "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) hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None)
service.async_set_service_schema( service.async_set_service_schema(
hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} hass, DOMAIN_LOGGER, "new_service", {"description": "new service"}