mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
2025.6.2 (#147355)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Joostlek <joostlek@outlook.com> Co-authored-by: Brian Rogers <brg468@hotmail.com> Co-authored-by: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Co-authored-by: starkillerOG <starkiller.og@gmail.com> Co-authored-by: Andre Lengwenus <alengwenus@gmail.com> Co-authored-by: Chris Talkington <chris@talkingtontech.com> Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com> Co-authored-by: elmurato <1382097+elmurato@users.noreply.github.com> Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com> Co-authored-by: Hessel <hesselonline@users.noreply.github.com> Co-authored-by: Ernst Klamer <e.klamer@gmail.com> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Ravaka Razafimanantsoa <3774520+SeraphicRav@users.noreply.github.com> Co-authored-by: Allen Porter <allen.porter@gmail.com> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: Brett Adams <Bre77@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: hahn-th <15319212+hahn-th@users.noreply.github.com> Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Joakim Sørensen <joasoe@proton.me> Co-authored-by: Michael Hansen <mike@rhasspy.org> Fix blocking open in Minecraft Server (#146820) Fix missing key for ecosmart in older Wallbox models (#146847) Fix device type filtering in sensor (#146945) Fix incorrect use of zip in service.async_get_all_descriptions (#147013) Fix Shelly entity names for gen1 sleeping devices (#147019) Fix log in onedrive (#147029) Fix Charge Cable binary sensor in Teslemetry (#147136) fix too many requests by API (#147197) Fix reload for Shelly devices with no script support (#147344)
This commit is contained in:
commit
773c25041a
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -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"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"domain": "switchbot",
|
"domain": "switchbot",
|
||||||
"name": "SwitchBot",
|
"name": "SwitchBot",
|
||||||
"integrations": ["switchbot", "switchbot_cloud"]
|
"integrations": ["switchbot", "switchbot_cloud"],
|
||||||
|
"iot_standards": ["matter"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
],
|
],
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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."]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 = {
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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)}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}"
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
try:
|
||||||
data = WEBHOOK_SCHEMA(dict(request.query))
|
requestdata = _parse_json_body(await request.json())
|
||||||
except vol.MultipleInvalid as error:
|
except JSONDecodeError as error:
|
||||||
|
LOGGER.error("Error parsing JSON body: %s", error)
|
||||||
return web.Response(
|
return web.Response(
|
||||||
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
|
text="Invalid JSON",
|
||||||
|
status=HTTPStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = WEBHOOK_SCHEMA(requestdata)
|
||||||
|
except vol.MultipleInvalid as error:
|
||||||
|
LOGGER.warning(humanize_error(requestdata, error))
|
||||||
|
return web.Response(
|
||||||
|
text=error.error_message,
|
||||||
|
status=HTTPStatus.UNPROCESSABLE_ENTITY,
|
||||||
)
|
)
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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,6 +139,7 @@ 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."""
|
||||||
|
try:
|
||||||
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
|
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
|
||||||
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
|
||||||
CHARGER_MAX_CHARGING_CURRENT_KEY
|
CHARGER_MAX_CHARGING_CURRENT_KEY
|
||||||
@ -166,20 +169,35 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 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:
|
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
|
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
|
||||||
elif eco_smart_mode == 0:
|
elif eco_smart_mode == 0:
|
||||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
|
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
|
||||||
elif eco_smart_mode == 1:
|
elif eco_smart_mode == 1:
|
||||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
|
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
|
||||||
|
|
||||||
return data
|
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."""
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -63,7 +63,7 @@ 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
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
2
requirements.txt
generated
@ -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
32
requirements_all.txt
generated
@ -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
|
||||||
|
32
requirements_test_all.txt
generated
32
requirements_test_all.txt
generated
@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
|
|||||||
aioairzone==1.0.0
|
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
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
39
tests/components/switchbot_cloud/test_binary_sensor.py
Normal file
39
tests/components/switchbot_cloud/test_binary_sensor.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Test for the switchbot_cloud binary sensors."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from switchbot_api import Device
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import configure_integration
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unsupported_device_type(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_list_devices,
|
||||||
|
mock_get_status,
|
||||||
|
) -> None:
|
||||||
|
"""Test that unsupported device types do not create sensors."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="unsupported-id-1",
|
||||||
|
deviceName="unsupported-device",
|
||||||
|
deviceType="UnsupportedDevice",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.return_value = {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.BINARY_SENSOR]
|
||||||
|
):
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
|
||||||
|
# Assert no binary sensor entities were created for unsupported device type
|
||||||
|
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
|
||||||
|
assert len([e for e in entities if e.domain == "binary_sensor"]) == 0
|
@ -65,3 +65,29 @@ async def test_meter_no_coordinator_data(
|
|||||||
entry = await configure_integration(hass)
|
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
|
||||||
|
@ -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×tamp=2013-09-17T07:32:51Z&",
|
||||||
|
"odometer": 0,
|
||||||
|
"activity": {"type": "still"},
|
||||||
|
"timestamp": "2013-09-17T07:32:51Z",
|
||||||
|
"battery": {"level": 0.1, "is_charging": False},
|
||||||
|
},
|
||||||
|
"device_id": "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
req = await client.post(url, json=data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert req.status == HTTPStatus.OK
|
||||||
|
state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device_id']}")
|
||||||
|
assert state.state == STATE_NOT_HOME
|
||||||
|
assert state.attributes["gps_accuracy"] == 10.5
|
||||||
|
assert state.attributes["battery_level"] == 10.0
|
||||||
|
assert state.attributes["speed"] == 100.0
|
||||||
|
assert state.attributes["bearing"] == 105.32
|
||||||
|
assert state.attributes["altitude"] == 102.0
|
||||||
|
|
||||||
|
|
||||||
async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None:
|
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}"
|
||||||
|
@ -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:
|
||||||
|
@ -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,14 +21,14 @@ 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_CHARGING_POWER_KEY: 0,
|
||||||
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
|
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
|
||||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||||
@ -39,8 +36,6 @@ test_response = json.loads(
|
|||||||
CHARGER_ADDED_ENERGY_KEY: "44.697",
|
CHARGER_ADDED_ENERGY_KEY: "44.697",
|
||||||
CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
|
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(
|
||||||
|
@ -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
|
||||||
|
@ -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,19 +71,17 @@ 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),
|
||||||
)
|
):
|
||||||
|
|
||||||
with pytest.raises(ConnectionError):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"lock",
|
"lock",
|
||||||
SERVICE_LOCK,
|
SERVICE_LOCK,
|
||||||
@ -87,7 +90,46 @@ async def test_wallbox_lock_class_connection_error(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
with pytest.raises(ConnectionError):
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||||
|
new=Mock(return_value=authorisation_response),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||||
|
new=Mock(side_effect=ConnectionError),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||||
|
new=Mock(side_effect=ConnectionError),
|
||||||
|
),
|
||||||
|
pytest.raises(ConnectionError),
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"lock",
|
||||||
|
SERVICE_UNLOCK,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||||
|
new=Mock(return_value=authorisation_response),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||||
|
new=Mock(side_effect=http_429_error),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||||
|
new=Mock(side_effect=http_429_error),
|
||||||
|
),
|
||||||
|
pytest.raises(HomeAssistantError),
|
||||||
|
):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"lock",
|
"lock",
|
||||||
SERVICE_UNLOCK,
|
SERVICE_UNLOCK,
|
||||||
|
@ -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,19 +121,17 @@ 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),
|
||||||
)
|
):
|
||||||
|
|
||||||
with pytest.raises(ConnectionError):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"number",
|
"number",
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -137,26 +143,82 @@ async def test_wallbox_number_class_connection_error(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_wallbox_number_class_energy_price_connection_error(
|
async def test_wallbox_number_class_too_many_requests(
|
||||||
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):
|
|
||||||
|
async def test_wallbox_number_class_energy_price_update_failed(
|
||||||
|
hass: HomeAssistant, entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test wallbox sensor class."""
|
||||||
|
|
||||||
|
await setup_integration(hass, entry)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||||
|
new=Mock(return_value=authorisation_response),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||||
|
new=Mock(side_effect=http_429_error),
|
||||||
|
),
|
||||||
|
pytest.raises(HomeAssistantError),
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number",
|
||||||
|
SERVICE_SET_VALUE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||||
|
ATTR_VALUE: 1.1,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_wallbox_number_class_energy_price_update_connection_error(
|
||||||
|
hass: HomeAssistant, entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test wallbox sensor class."""
|
||||||
|
|
||||||
|
await setup_integration(hass, entry)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||||
|
new=Mock(return_value=authorisation_response),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||||
|
new=Mock(side_effect=http_404_error),
|
||||||
|
),
|
||||||
|
pytest.raises(HomeAssistantError),
|
||||||
|
):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"number",
|
"number",
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -175,19 +237,17 @@ 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(
|
||||||
mock_request.post(
|
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||||
"https://api.wall-box.com/chargers/config/12345",
|
new=Mock(side_effect=http_429_error),
|
||||||
json={CHARGER_ENERGY_PRICE_KEY: 1.1},
|
),
|
||||||
status_code=403,
|
pytest.raises(HomeAssistantError),
|
||||||
)
|
):
|
||||||
|
|
||||||
with pytest.raises(ConfigEntryAuthFailed):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"number",
|
"number",
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -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,19 +306,17 @@ 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(
|
||||||
mock_request.post(
|
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||||
"https://api.wall-box.com/chargers/config/12345",
|
new=Mock(side_effect=http_403_error),
|
||||||
json={CHARGER_MAX_ICP_CURRENT_KEY: 10},
|
),
|
||||||
status_code=403,
|
pytest.raises(InvalidAuth),
|
||||||
)
|
):
|
||||||
|
|
||||||
with pytest.raises(InvalidAuth):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
NUMBER_DOMAIN,
|
NUMBER_DOMAIN,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@ -280,19 +335,46 @@ 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(
|
||||||
with pytest.raises(ConnectionError):
|
NUMBER_DOMAIN,
|
||||||
|
SERVICE_SET_VALUE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||||
|
ATTR_VALUE: 10,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_wallbox_number_class_icp_energy_too_many_request(
|
||||||
|
hass: HomeAssistant, entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test wallbox sensor class."""
|
||||||
|
|
||||||
|
await setup_integration(hass, entry)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||||
|
new=Mock(return_value=authorisation_response),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||||
|
new=Mock(side_effect=http_429_error),
|
||||||
|
),
|
||||||
|
pytest.raises(HomeAssistantError),
|
||||||
|
):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
NUMBER_DOMAIN,
|
NUMBER_DOMAIN,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
|
@ -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,
|
||||||
|
@ -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,19 +67,18 @@ 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
|
||||||
with pytest.raises(ConnectionError):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"switch",
|
"switch",
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
@ -85,37 +87,27 @@ async def test_wallbox_switch_class_connection_error(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
with pytest.raises(ConnectionError):
|
|
||||||
await hass.services.async_call(
|
|
||||||
"switch",
|
|
||||||
SERVICE_TURN_OFF,
|
|
||||||
{
|
|
||||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_wallbox_switch_class_authentication_error(
|
async def test_wallbox_switch_class_too_many_requests(
|
||||||
hass: HomeAssistant, entry: MockConfigEntry
|
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(
|
||||||
mock_request.post(
|
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||||
"https://api.wall-box.com/v3/chargers/12345/remote-action",
|
new=Mock(side_effect=http_429_error),
|
||||||
json={CHARGER_STATUS_ID_KEY: 193},
|
),
|
||||||
status_code=403,
|
pytest.raises(HomeAssistantError),
|
||||||
)
|
):
|
||||||
|
# Test behavior when a connection error occurs
|
||||||
with pytest.raises(ConfigEntryAuthFailed):
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"switch",
|
"switch",
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
@ -124,12 +116,3 @@ async def test_wallbox_switch_class_authentication_error(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
with pytest.raises(ConfigEntryAuthFailed):
|
|
||||||
await hass.services.async_call(
|
|
||||||
"switch",
|
|
||||||
SERVICE_TURN_OFF,
|
|
||||||
{
|
|
||||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
|
@ -21,7 +21,6 @@ ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
|
|||||||
VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3"
|
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"
|
||||||
|
@ -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,
|
||||||
|
7543
tests/components/zwave_js/fixtures/ring_keypad_state.json
Normal file
7543
tests/components/zwave_js/fixtures/ring_keypad_state.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -97,8 +97,8 @@
|
|||||||
'value_id': '52-113-0-Home Security-Cover status',
|
'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',
|
||||||
|
@ -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
|
||||||
|
@ -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"] == {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user